diff --git a/ChangeLog b/ChangeLog index f749e20f6..4252b6f00 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,38 @@ * SoObjects/SOGo/SOGoObject.m (-initWithName:inContainer:): make sure that "_name" is neither nil nor empty. +2012-06-27 Jean Raby + + * SoObjects/Appointments/SOGoAppointmentObject.m + (PUTAction:): detect conflicting event UID and + deny the request accordingly. + +2012-06-21 Ludovic Marcotte + + * Added the SOGoSearchMinimumWordLength domain + default which controls the minimal length required + before trigging server-side search operations for + attendee completion, contact searches, etc. The + default value is 2, which means search operations + are trigged once the 3rd character is typed. + +2012-06-20 Ludovic Marcotte + + * SoObjects/Appointments/SOGoAppointmentObject.m + (-_handleResourcesConflicts:forEvent:): We now + handle correctly recurring events overlapping other + recurring events. + +2012-06-19 Ludovic Marcotte + + * SoObjects/Appointments/SOGoAppointmentFolder.m (-importCalendar:): + We now handle correctly floating events by forcing the use of + the user's timezone. + + * SoObjects/Appointments/SOGoCalendarComponent.m (-expandGroupsInEvent:): + We now remove all attendees that are equal (email-based comparison) to + the event's organizer instead of only for decomposed groups. + 2012-06-12 Wolfgang Sourdeau * SoObjects/SOGo/SOGoGroup.m diff --git a/Documentation/SOGo Installation Guide.odt b/Documentation/SOGo Installation Guide.odt index dd67e76e6..843a35b75 100644 Binary files a/Documentation/SOGo Installation Guide.odt and b/Documentation/SOGo Installation Guide.odt differ diff --git a/NEWS b/NEWS index fc4946c96..c0d3fbf2e 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,14 @@ +1.3.17 (2012-MM-DD) +------------------- +New Features + - + +Enhancements + - updated Czech translation + +Bug Fixes + - + 1.3.16 (2012-06-07) ------------------- Enhancements @@ -8,6 +19,7 @@ Enhancements - it's no longer possible to click the "Upload" button multiple times - allow delivery of mail with no subject, but alert the user - updated Dutch, German, French translations + Bug Fixes - fixed compilation under GNU/kFreeBSD - fixed compilation for arm architecture diff --git a/SOPE/GDLContentStore/GCSSpecialQueries.m b/SOPE/GDLContentStore/GCSSpecialQueries.m index 34a29ab82..d3c916864 100644 --- a/SOPE/GDLContentStore/GCSSpecialQueries.m +++ b/SOPE/GDLContentStore/GCSSpecialQueries.m @@ -180,8 +180,8 @@ - (NSString *) createSessionsFolderWithName: (NSString *) tableName { static NSString *sqlFolderFormat - = (@"CREATE TABLE %@ (" - @" c_id VARCHAR(255) PRIMARY KEY," + = (@"CREATE TABLE %@ (" + @" c_id VARCHAR(255) NOT NULL PRIMARY KEY," @" c_value VARCHAR(255) NOT NULL," @" c_creationdate INT4 NOT NULL," @" c_lastseen INT4 NOT NULL)"); diff --git a/SoObjects/Appointments/Czech.lproj/Localizable.strings b/SoObjects/Appointments/Czech.lproj/Localizable.strings index fe8c3594f..9053f8dab 100644 --- a/SoObjects/Appointments/Czech.lproj/Localizable.strings +++ b/SoObjects/Appointments/Czech.lproj/Localizable.strings @@ -67,4 +67,4 @@ vtodo_class2 = "(Skrytý úkol)"; = "%{Attendee} %{SentByText}dosud o Vaší pozvánce k události nerozhodl/a."; /* Resources */ -"Maximum number of simultaneous bookings (%{NumberOfSimultaneousBookings}) reached for resource \"%{Cn} %{SystemEmail}\"." = "Maximální počet současných rezervací (%{NumberOfSimultaneousBookings}) byl dosažen pro zdroj \"%{Cn} %{SystemEmail}\"."; \ No newline at end of file +"Maximum number of simultaneous bookings (%{NumberOfSimultaneousBookings}) reached for resource \"%{Cn} %{SystemEmail}\". The conflicting event is \"%{EventTitle}\", and starts on %{StartDate}." = "Byl dosažen maximální počet současných rezervací\n(%{NumberOfSimultaneousBookings}) pro zdroj \"%{Cn} %{SystemEmail}\". Konfliktní událost je \"%{EventTitle}\" a začíná %{StartDate}."; \ No newline at end of file diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index d0b0b0969..0a2dd4b18 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -2764,7 +2764,7 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir iCalEvent *event; int imported, count, i; - + imported = 0; if (calendar) @@ -2799,6 +2799,41 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir tzId = [startDate value: 0 ofAttribute: @"tzid"]; if ([tzId length]) timezone = [timezones valueForKey: tzId]; + else + { + // If the start date is a "floating time", let's use the user's timezone + // during the import for both the start and end dates. + NSString *s; + + s = [[startDate valuesAtIndex: 0 forKey: @""] objectAtIndex: 0]; + + if ([element isKindOfClass: [iCalEvent class]] && + ![(iCalEvent *)element isAllDay] && + ![s hasSuffix: @"Z"] && + ![s hasSuffix: @"z"]) + { + iCalDateTime *endDate; + int delta; + + timezone = [iCalTimeZone timeZoneForName: [[[self->context activeUser] userDefaults] timeZoneName]]; + [calendar addTimeZone: timezone]; + + delta = [[timezone periodForDate: [startDate dateTime]] secondsOffsetFromGMT]; + event = (iCalEvent *)element; + + [event setStartDate: [[event startDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: -delta]]; + [startDate setTimeZone: timezone]; + + endDate = (iCalDateTime *) [element uniqueChildWithTag: @"dtend"]; + + if (endDate) + { + [event setEndDate: [[event endDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: -delta]]; + [endDate setTimeZone: timezone]; + } + } + } + if ([element isKindOfClass: [iCalEvent class]]) { event = (iCalEvent *)element; diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 596e2d2ba..ad50e0ddf 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -1,5 +1,5 @@ /* - Copyright (C) 2007-2011 Inverse inc. + Copyright (C) 2007-2012 Inverse inc. Copyright (C) 2004-2005 SKYRIX Software AG This file is part of SOGo @@ -456,8 +456,12 @@ { SOGoAppointmentFolder *folder; NSCalendarDate *start, *end; + NGCalendarDateRange *range; NSMutableArray *fbInfo; - int i; + NSArray *allOccurences; + + BOOL must_delete; + int i, j; // We get the start/end date for our conflict range. If the event to be added is recurring, we // check for at least a year to start with. @@ -486,12 +490,50 @@ // We first remove any occurences in the freebusy that corresponds to the // current event. We do this to avoid raising a conflict if we move a 1 hour // meeting from 12:00-13:00 to 12:15-13:15. We would overlap on ourself otherwise. + // + // We must also check here for repetitive events that don't overlap our event. + // We remove all events that don't overlap. The events here are already + // decomposed. + // + if ([theEvent isRecurrent]) + allOccurences = [theEvent recurrenceRangesWithinCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: start + endDate: end] + firstInstanceCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: [theEvent startDate] + endDate: [theEvent endDate]]]; + else + allOccurences = nil; + for (i = [fbInfo count]-1; i >= 0; i--) { + range = [NGCalendarDateRange calendarDateRangeWithStartDate: [[fbInfo objectAtIndex: i] objectForKey: @"startDate"] + endDate: [[fbInfo objectAtIndex: i] objectForKey: @"endDate"]]; + if ([[[fbInfo objectAtIndex: i] objectForKey: @"c_uid"] compare: [theEvent uid]] == NSOrderedSame) - [fbInfo removeObjectAtIndex: i]; - } + { + [fbInfo removeObjectAtIndex: i]; + continue; + } + + // No need to check if the event isn't recurrent here as it's handled correctly + // when we compute the "end" date. + if ([allOccurences count]) + { + must_delete = YES; + for (j = 0; j < [allOccurences count]; j++) + { + if ([range doesIntersectWithDateRange: [allOccurences objectAtIndex: j]]) + { + must_delete = NO; + break; + } + } + + if (must_delete) + [fbInfo removeObjectAtIndex: i]; + } + } + if ([fbInfo count]) { // If we always force the auto-accept if numberOfSimultaneousBookings == 0 (ie., no limit @@ -1714,15 +1756,25 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent { iCalCalendar *calendar; SOGoUser *ownerUser; - iCalEvent *event; + iCalEvent *event, *conflictingEvent; + NSString *eventUID; BOOL scheduling; calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; event = [[calendar events] objectAtIndex: 0]; + eventUID = [event uid]; ownerUser = [SOGoUser userWithLogin: owner]; scheduling = [self _shouldScheduleEvent: [event organizer]]; + + // make sure eventUID doesn't conflict with an existing event - see bug #1853 + // TODO: send out a no-uid-conflict (DAV:href) xml element (rfc4791 section 5.3.2.1) + if (conflictingEvent = [container resourceNameForEventUID: eventUID]) + { + NSString *reason = [NSString stringWithFormat: @"Event UID already in use. (%s)", eventUID]; + return [NSException exceptionWithHTTPStatus:403 reason: reason]; + } // // New event and we're the organizer -- send invitation to all attendees diff --git a/SoObjects/Appointments/SOGoCalendarComponent.m b/SoObjects/Appointments/SOGoCalendarComponent.m index 2bedbaa9d..9719dcab2 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.m +++ b/SoObjects/Appointments/SOGoCalendarComponent.m @@ -516,19 +516,23 @@ static inline BOOL _occurenceHasID (iCalRepeatableEntityObject *occurence, // // Returs "YES" if a a group was decomposed among attendees. // +// It can also return yes if an attendee was found in the list +// matching the organizer. In which case, it was removed. +// - (BOOL) expandGroupsInEvent: (iCalEvent *) theEvent { - NSMutableArray *allAttendees; - NSEnumerator *enumerator; NSString *organizerEmail, *domain; + NSMutableArray *allAttendees; iCalPerson *currentAttendee; + NSEnumerator *enumerator; SOGoGroup *group; - BOOL doesIncludeGroup; + + BOOL eventWasModified; unsigned int i; domain = [[context activeUser] domain]; organizerEmail = [[theEvent organizer] rfc822Email]; - doesIncludeGroup = NO; + eventWasModified = NO; allAttendees = [NSMutableArray arrayWithArray: [theEvent attendees]]; enumerator = [[theEvent attendees] objectEnumerator]; while ((currentAttendee = [enumerator nextObject])) @@ -548,7 +552,7 @@ static inline BOOL _occurenceHasID (iCalRepeatableEntityObject *occurence, for (i = 0; i < [members count]; i++) { user = [members objectAtIndex: i]; - doesIncludeGroup = YES; + eventWasModified = YES; // If the organizer is part of the group, we skip it from // the addition to the attendees' list @@ -565,12 +569,23 @@ static inline BOOL _occurenceHasID (iCalRepeatableEntityObject *occurence, [allAttendees addObject: person]; } } - } + else + { + // We remove any attendees matching the organizer. Apple iCal will do that when + // you invite someone. It'll add the organizer in the attendee list, which will + // confuse itself! + if ([[currentAttendee rfc822Email] caseInsensitiveCompare: organizerEmail] == NSOrderedSame) + { + [allAttendees removeObject: currentAttendee]; + eventWasModified = YES; + } + } + } // while (currentAttendee ... - if (doesIncludeGroup) + if (eventWasModified) [theEvent setAttendees: allAttendees]; - return doesIncludeGroup; + return eventWasModified; } - (void) _updateRecurrenceIDsWithEvent: (iCalRepeatableEntityObject*) newEvent diff --git a/SoObjects/SOGo/SOGoDefaults.plist b/SoObjects/SOGo/SOGoDefaults.plist index c686a1665..127ddc2a8 100644 --- a/SoObjects/SOGo/SOGoDefaults.plist +++ b/SoObjects/SOGo/SOGoDefaults.plist @@ -70,4 +70,6 @@ SOGoReminderEnabled = YES; SOGoRemindWithASound = YES; + + SOGoSearchMinimumWordLength = 2; } diff --git a/SoObjects/SOGo/SOGoDomainDefaults.h b/SoObjects/SOGo/SOGoDomainDefaults.h index 2c0c86b94..fbed067ee 100644 --- a/SoObjects/SOGo/SOGoDomainDefaults.h +++ b/SoObjects/SOGo/SOGoDomainDefaults.h @@ -77,6 +77,8 @@ - (BOOL) hideSystemEMail; +- (int) searchMinimumWordLength; + @end #endif /* SOGODOMAINDEFAULTS_H */ diff --git a/SoObjects/SOGo/SOGoDomainDefaults.m b/SoObjects/SOGo/SOGoDomainDefaults.m index bb06640fe..6fe2abb7a 100644 --- a/SoObjects/SOGo/SOGoDomainDefaults.m +++ b/SoObjects/SOGo/SOGoDomainDefaults.m @@ -319,4 +319,9 @@ return [self boolForKey: @"SOGoHideSystemEMail"]; } +- (int) searchMinimumWordLength +{ + return [self integerForKey: @"SOGoSearchMinimumWordLength"]; +} + @end diff --git a/Tests/Integration/test-caldav-scheduling.py b/Tests/Integration/test-caldav-scheduling.py index 4e6a4a268..06b8e2991 100755 --- a/Tests/Integration/test-caldav-scheduling.py +++ b/Tests/Integration/test-caldav-scheduling.py @@ -4,6 +4,9 @@ # attendee1_delegate_username and superuser. # when writing new tests, avoid using superuser when not absolutely needed +# TODO +# - Individual tests should set the ACLs themselves on Resources tests + from config import hostname, port, username, password, \ superuser, superuser_password, \ attendee1, attendee1_username, \ @@ -124,6 +127,8 @@ class CalDAVSchedulingTest(unittest.TestCase): self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1 self.attendee1_delegate_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1_delegate + self.res_calendar = "/SOGo/dav/%s/Calendar/personal/" % resource_no_overbook + self.res_ob_calendar = "/SOGo/dav/%s/Calendar/personal/" % resource_can_overbook # fetch non existing event to let sogo create the calendars in the db self._getEvent(self.client, "%snonexistent" % self.user_calendar, exp_status=404) @@ -131,35 +136,24 @@ class CalDAVSchedulingTest(unittest.TestCase): self._getEvent(self.attendee1_delegate_client, "%snonexistent" % self.attendee1_delegate_calendar, exp_status=404) + # list of ics used by the test. + # tearDown will loop over this and wipe them in all users' calendar + self.ics_list = [] + def tearDown(self): - self._deleteEvent(self.client, - "%stest-delegation.ics" % self.user_calendar, None) - self._deleteEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar, None) - self._deleteEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" % self.attendee1_delegate_calendar, - None) - self._deleteEvent(self.client, - "%stest-add-attendee.ics" % self.user_calendar, None) - self._deleteEvent(self.attendee1_client, - "%stest-add-attendee.ics" % self.attendee1_calendar, None) - self._deleteEvent(self.client, - "%stest-no-overbook.ics" % self.user_calendar, None) - self._deleteEvent(self.client, - "%stest-no-overbook-overlap.ics" % self.user_calendar, None) - self._deleteEvent(self.client, - "%stest-can-overbook.ics" % self.user_calendar, None) - self._deleteEvent(self.client, - "%stest-can-overbook-overlap.ics" % self.user_calendar, None) - self._deleteEvent(self.client, - "%stest-rrule-exception-invitation-dance.ics" % self.user_calendar, None) - self._deleteEvent(self.attendee1_client, - "%stest-rrule-exception-invitation-dance.ics" % self.attendee1_calendar, None) - self._deleteEvent(self.client, - "%stest-rrule-invitation-deleted-exdate-dance.ics" % self.user_calendar, None) - self._deleteEvent(self.attendee1_client, - "%stest-rrule-invitation-deleted-exdate-dance.ics" % self.attendee1_calendar, None) + # delete all created events from all users' calendar + for ics in self.ics_list: + self._deleteEvent(self.superuser_client, + "%s%s" % (self.user_calendar, ics), None) + self._deleteEvent(self.superuser_client, + "%s%s" % (self.attendee1_calendar, ics), None) + self._deleteEvent(self.superuser_client, + "%s%s" % (self.attendee1_delegate_calendar, ics), None) + self._deleteEvent(self.superuser_client, + "%s%s" % (self.res_calendar, ics), None) + self._deleteEvent(self.superuser_client, + "%s%s" % (self.res_ob_calendar, ics), None) def _newEvent(self, summary="test event", uid="test", transp=0): transparency = ("OPAQUE", "TRANSPARENT") @@ -219,6 +213,25 @@ class CalDAVSchedulingTest(unittest.TestCase): if exp_status is not None: self.assertEquals(delete.response["status"], exp_status) + def _getAllEvents(self, client, collection, exp_status = 207): + propfind = webdavlib.WebDAVPROPFIND(collection, None) + client.execute(propfind) + if exp_status is not None: + self.assertEquals(propfind.response["status"], exp_status) + + content = [] + nodes = propfind.response["document"].findall('{DAV:}response') + for node in nodes: + responseHref = node.find('{DAV:}href').text + content += [responseHref] + + return content + + def _deleteAllEvents(self, client, collection, exp_status = 204): + content = self._getAllEvents(client, collection) + for item in content: + self._deleteEvent(client, item) + def _eventAttendees(self, event): attendees = {} @@ -268,7 +281,9 @@ class CalDAVSchedulingTest(unittest.TestCase): """ add attendee after event creation """ # make sure the event doesn't exist - ics_name = "test-add-attendee.ics" + ics_name = "test-add-attendee.ics" + self.ics_list += [ics_name] + self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) self._deleteEvent(self.attendee1_client, @@ -304,6 +319,8 @@ class CalDAVSchedulingTest(unittest.TestCase): # make sure the event doesn't exist ics_name = "test-remove-attendee.ics" + self.ics_list += [ics_name] + self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) self._deleteEvent(self.attendee1_client, @@ -346,104 +363,80 @@ class CalDAVSchedulingTest(unittest.TestCase): # 6. verify that the attendee doesn't have the event anymore attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name), 404) - def testAddAttendee(self): - """ add attendee after event creation """ + def testResourceNoOverbook(self): + """ try to overbook a resource """ + + # make sure there are no events in the resource calendar + self._deleteAllEvents(self.superuser_client, self.res_calendar) # make sure the event doesn't exist - ics_name = "test-add-attendee.ics" - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - self._deleteEvent(self.client, - "%s%s" % (self.attendee1_calendar,ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test add attendee", uid="Test add attendee") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. add an attendee - attendee = event.vevent.add('attendee') - attendee.cn_param = self.attendee1_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.attendee1_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event, - exp_status=204) - - - # 3. verify that the attendee has the event - attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, ics_name)) - - # 4. make sure the received event match the original one - # XXX is this enough? - self.assertEquals(event.vevent.uid, attendee_event.vevent.uid) - - def testResourceNoOverbook(self): - """ try to overbook a resource """ - - # make sure the event doesn't exist - ics_name = "test-no-overbook.ics" + ics_name = "test-no-overbook.ics" + self.ics_list += [ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ics_name), None) - ob_ics_name = "test-no-overbook-overlap.ics" - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - - # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test no overbook", uid="test no overbook") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_no_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_no_ob_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) - - # 2. create a second event overlapping the first one - event = self._newEvent(summary="Test no overbook - overlap", uid="test no overbook - overlap") - organizer = event.vevent.add('organizer') - organizer.cn_param = self.user_name - organizer.value = self.user_email - attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_no_ob_name - attendee.rsvp_param = "TRUE" - attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_no_ob_email - - # put the event - should trigger a 403 - self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event, exp_status=403) - - def testResourceCanOverbook(self): - """ try to overbook a resource - multiplebookings=0""" - - # make sure the event doesn't exist - ics_name = "test-can-overbook.ics" - self._deleteEvent(self.client, - "%s%s" % (self.user_calendar,ics_name), None) - - ob_ics_name = "test-can-overbook-overlap.ics" + ob_ics_name = "test-no-overbook-overlap.ics" + self.ics_list += [ob_ics_name] self._deleteEvent(self.client, "%s%s" % (self.user_calendar,ob_ics_name), None) # 1. create an event in the organiser's calendar - event = self._newEvent(summary="Test can overbook", uid="test can overbook") + event = self._newEvent(summary="Test no overbook", uid="test no overbook") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email attendee = event.vevent.add('attendee') - attendee.cn_param = self.res_can_ob_name + attendee.cn_param = self.res_no_ob_name attendee.rsvp_param = "TRUE" attendee.partstat_param = "NEEDS-ACTION" - attendee.value = self.res_can_ob_email - self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) + attendee.value = self.res_no_ob_email + self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) # 2. create a second event overlapping the first one - event = self._newEvent(summary="Test can overbook - overlap", uid="test can overbook - overlap") + event = self._newEvent(summary="Test no overbook - overlap", uid="test no overbook - overlap") + organizer = event.vevent.add('organizer') + organizer.cn_param = self.user_name + organizer.value = self.user_email + attendee = event.vevent.add('attendee') + attendee.cn_param = self.res_no_ob_name + attendee.rsvp_param = "TRUE" + attendee.partstat_param = "NEEDS-ACTION" + attendee.value = self.res_no_ob_email + + # put the event - should trigger a 403 + self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event, exp_status=403) + + def testResourceCanOverbook(self): + """ try to overbook a resource - multiplebookings=0""" + + # make sure there are no events in the resource calendar + self._deleteAllEvents(self.superuser_client, self.res_ob_calendar) + + # make sure the event doesn't exist + ics_name = "test-can-overbook.ics" + self.ics_list += [ics_name] + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar,ics_name), None) + + ob_ics_name = "test-can-overbook-overlap.ics" + self.ics_list += [ob_ics_name] + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar,ob_ics_name), None) + + # 1. create an event in the organiser's calendar + event = self._newEvent(summary="Test can overbook", uid="test can overbook") + organizer = event.vevent.add('organizer') + organizer.cn_param = self.user_name + organizer.value = self.user_email + attendee = event.vevent.add('attendee') + attendee.cn_param = self.res_can_ob_name + attendee.rsvp_param = "TRUE" + attendee.partstat_param = "NEEDS-ACTION" + attendee.value = self.res_can_ob_email + self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) + + # 2. create a second event overlapping the first one + event = self._newEvent(summary="Test can overbook - overlap", uid="test can overbook - overlap") organizer = event.vevent.add('organizer') organizer.cn_param = self.user_name organizer.value = self.user_email @@ -453,8 +446,97 @@ class CalDAVSchedulingTest(unittest.TestCase): attendee.partstat_param = "NEEDS-ACTION" attendee.value = self.res_can_ob_email - # put the event - should be fine since we can overbook this one - self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event) + # put the event - should be fine since we can overbook this one + self._putEvent(self.client, "%s%s" % (self.user_calendar, ob_ics_name), event) + + def testResourceBookingOverlapDetection(self): + """ Resource booking overlap detection - bug #1837""" + + # There used to be some problems with recurring events and resources booking + # This test implements these edge cases + + # 1. Create recurring event (with resource) + # 2. Create single event overlaping one instance for the previous event + # (should fail) + # 3. Create recurring event which _doesn't_ overlap the first event + # (should be OK, used to fail pre1.3.17) + # 4. Create recurring event overlapping the previous recurring event + # (should fail) + + # make sure there are no events in the resource calendar + self._deleteAllEvents(self.superuser_client, self.res_calendar) + + # make sure the event doesn't exist + ics_name = "test-res-overlap-detection.ics" + self.ics_list += [ics_name] + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar,ics_name), None) + + overlap_ics_name = "test-res-overlap-detection-overlap.ics" + self.ics_list += [overlap_ics_name] + self._deleteEvent(self.client, + "%s%s" % (self.attendee1_calendar,overlap_ics_name), None) + + nooverlap_recurring_ics_name = "test-res-overlap-detection-nooverlap.ics" + self.ics_list += [nooverlap_recurring_ics_name] + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar,nooverlap_recurring_ics_name), None) + + overlap_recurring_ics_name = "test-res-overlap-detection-overlap-recurring.ics" + self.ics_list += [overlap_recurring_ics_name] + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar,overlap_recurring_ics_name), None) + + # 1. create recurring event with resource + event = self._newEvent(summary="recurring event with resource", + uid="recurring event w resource") + event.vevent.add('rrule').value = "FREQ=DAILY;COUNT=5" + organizer = event.vevent.add('organizer') + organizer.cn_param = self.user_name + organizer.value = self.user_email + attendee = event.vevent.add('attendee') + attendee.cn_param = self.res_no_ob_name + attendee.rsvp_param = "TRUE" + attendee.partstat_param = "NEEDS-ACTION" + attendee.value = self.res_no_ob_email + + # keep a copy around for #3 + nooverlap_event = vobject.iCalendar() + nooverlap_event.copy(event) + + self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) + + # 2. Create single event overlaping one instance for the previous event + event = self._newEvent(summary="recurring event with resource", + uid="recurring event w resource - overlap") + organizer = event.vevent.add('organizer') + organizer.cn_param = self.attendee1_name + organizer.value = self.attendee1_email + attendee = event.vevent.add('attendee') + attendee.cn_param = self.res_no_ob_name + attendee.rsvp_param = "TRUE" + attendee.partstat_param = "NEEDS-ACTION" + attendee.value = self.res_no_ob_email + # should fail + self._putEvent(self.client, "%s%s" % (self.attendee1_calendar, overlap_ics_name), event, exp_status=403) + + # 3. Create recurring event which _doesn't_ overlap the first event + # (should be OK, used to fail pre1.3.17) + # shift the start date to one hour after the original event end time + nstartdate = nooverlap_event.vevent.dtend.value + datetime.timedelta(0, 3600) + nooverlap_event.vevent.dtstart.value = nstartdate + nooverlap_event.vevent.dtend.value = nstartdate + datetime.timedelta(0, 3600) + nooverlap_event.vevent.uid.value = "recurring - nooverlap" + + self._putEvent(self.client, "%s%s" % (self.user_calendar, nooverlap_recurring_ics_name), nooverlap_event) + + # 4. Create recurring event overlapping the previous recurring event + # should fail + nstartdate = nooverlap_event.vevent.dtstart.value + datetime.timedelta(0, 300) + nooverlap_event.vevent.dtstart.value = nstartdate + nooverlap_event.vevent.dtend.value = nstartdate + datetime.timedelta(0, 3600) + nooverlap_event.vevent.uid.value = "recurring - overlap" + self._putEvent(self.client, "%s%s" % (self.user_calendar, overlap_recurring_ics_name), nooverlap_event, exp_status=403) def testRruleExceptionInvitationDance(self): @@ -474,6 +556,8 @@ class CalDAVSchedulingTest(unittest.TestCase): # bob isn't in the master+exception event ics_name = "test-rrule-exception-invitation-dance.ics" + self.ics_list += [ics_name] + self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, @@ -596,6 +680,8 @@ class CalDAVSchedulingTest(unittest.TestCase): # and that bob is 'declined' ics_name = "test-rrule-invitation-deleted-exdate-dance.ics" + self.ics_list += [ics_name] + self._deleteEvent(self.client, "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, @@ -663,17 +749,89 @@ class CalDAVSchedulingTest(unittest.TestCase): self.assertEqual(org_ev_master.attendee.partstat_param, "NEEDS-ACTION"); self.assertEqual(org_ev_exception.attendee.partstat_param, "DECLINED"); + def testOrganizerIsAttendee(self): + """ iCal organizer is attendee - bug #1839 """ + + # This tries to have the same behavior as iCal + # 1. create an event, add an attendee and add the organizer as an attendee + # 2. SOGo should remove the organizer from the attendee list + ics_name = "test-organizer-is-attendee.ics" + self.ics_list += [ics_name] + + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar, ics_name), None) + self._deleteEvent(self.attendee1_client, + "%s%s" % (self.attendee1_calendar, ics_name), None) + + # 1. create a recurring event in the organiser's calendar + summary="org is attendee" + uid=summary + event = self._newEvent(summary, uid) + organizer = event.vevent.add('organizer') + organizer.cn_param = self.user_name + organizer.partstat_param = "ACCEPTED" + organizer.value = self.user_email + attendee = event.vevent.add('attendee') + attendee.cn_param = self.attendee1_name + attendee.rsvp_param = "TRUE" + attendee.role_param = "REQ-PARTICIPANT" + attendee.partstat_param = "NEEDS-ACTION" + attendee.value = self.attendee1_email + + # 1.1 add the organizer as an attendee + attendee = event.vevent.add('attendee') + attendee.cn_param = self.user_name + attendee.rsvp_param = "TRUE" + attendee.role_param = "REQ-PARTICIPANT" + attendee.partstat_param = "ACCEPTED" + attendee.value = self.user_email + + self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) + + # 2. Fetch the event and make sure the organizer is not in the attendee list anymore + org_ev = self._getEvent(self.client, "%s%s" % (self.user_calendar, ics_name)) + + for attendee in org_ev.vevent.attendee_list: + self.assertNotEqual(self.user_email, attendee.value) + + def testEventsWithSameUID(self): + """ PUT 2 events with the same UID - bug #1853 """ + + ics_name = "test-same-uid.ics" + self.ics_list += [ics_name] + + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar, ics_name), None) + + conflict_ics_name = "test-same-uid-conflict.ics" + self.ics_list += [ics_name] + + self._deleteEvent(self.client, + "%s%s" % (self.user_calendar, conflict_ics_name), None) + + # 1. create simple event + summary="same uid" + uid=summary + event = self._newEvent(summary, uid) + + self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event) + + # PUT the same event with a new filename - should trigger a 403 + self._putEvent(self.client, "%s%s" % (self.user_calendar, conflict_ics_name), event, exp_status=403) + def testInvitationDelegation(self): """ invitation delegation """ + ics_name = "test-delegation.ics" + self.ics_list += [ics_name] + # the invitation must not exist self._deleteEvent(self.client, - "%stest-delegation.ics" % self.user_calendar, None) + "%s%s" % (self.user_calendar, ics_name), None) self._deleteEvent(self.attendee1_client, - "%stest-delegation.ics" % self.attendee1_calendar, None) + "%s%s" % (self.attendee1_calendar, ics_name), None) self._deleteEvent(self.attendee1_delegate_client, - "%stest-delegation.ics" % self.attendee1_delegate_calendar, - None) + "%s%s" % (self.attendee1_delegate_calendar, ics_name), None) # 1. org -> attendee => org: 1, attendee: 1 (pst=N-A), delegate: 0 diff --git a/Tools/SOGoToolExpireUserSessions.m b/Tools/SOGoToolExpireUserSessions.m index 8eb530f3b..8da66f3d9 100644 --- a/Tools/SOGoToolExpireUserSessions.m +++ b/Tools/SOGoToolExpireUserSessions.m @@ -109,7 +109,7 @@ return rc=NO; } - sql = [NSString stringWithFormat: @"SELECT count(*) FROM %@ WHERE c_lastseen <= %d;", + sql = [NSString stringWithFormat: @"SELECT count(*) FROM %@ WHERE c_lastseen <= %d", [tableURL gcsTableName], oldest]; ex = [channel evaluateExpressionX: sql]; if (ex) @@ -129,7 +129,7 @@ if (verbose) NSLog(@"Will be removing %d sessions", sessionsToDelete); [channel cancelFetch]; - sql = [NSString stringWithFormat: @"DELETE FROM %@ WHERE c_lastseen <= %d;", + sql = [NSString stringWithFormat: @"DELETE FROM %@ WHERE c_lastseen <= %d", [tableURL gcsTableName], oldest]; if (verbose) NSLog(@"Removing sessions older than %d minute(s)", nbMinutes); diff --git a/UI/Common/UIxPageFrame.m b/UI/Common/UIxPageFrame.m index ee990ad73..0b3282e47 100644 --- a/UI/Common/UIxPageFrame.m +++ b/UI/Common/UIxPageFrame.m @@ -1,15 +1,16 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG + Copyright (C) 2005-2012 Inverse inc. - This file is part of OpenGroupware.org. + This file is part of SOGo. - OGo is free software; you can redistribute it and/or modify it under + SOGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. - OGo is distributed in the hope that it will be useful, but WITHOUT ANY + SOGo 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 Lesser General Public License for more details. @@ -590,4 +591,9 @@ ); } +- (int) minimumSearchLength +{ + return [[[context activeUser] domainDefaults] searchMinimumWordLength]; +} + @end /* UIxPageFrame */ diff --git a/UI/Contacts/UIxContactsFilterPanel.m b/UI/Contacts/UIxContactsFilterPanel.m index 55fa93af5..a0f5721bb 100644 --- a/UI/Contacts/UIxContactsFilterPanel.m +++ b/UI/Contacts/UIxContactsFilterPanel.m @@ -1,14 +1,15 @@ /* Copyright (C) 2000-2005 SKYRIX Software AG + Copyright (C) 2000-2012 Inverse inc. - This file is part of OpenGroupware.org. + This file is part of SOGo. - OGo is free software; you can redistribute it and/or modify it under + SOGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. - OGo is distributed in the hope that it will be useful, but WITHOUT ANY + SOGo 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 Lesser General Public License for more details. diff --git a/UI/Contacts/UIxContactsUserFolders.m b/UI/Contacts/UIxContactsUserFolders.m index 1ff941edf..ba4dad22f 100644 --- a/UI/Contacts/UIxContactsUserFolders.m +++ b/UI/Contacts/UIxContactsUserFolders.m @@ -1,6 +1,6 @@ /* UIxContactsUserFolders.m - this file is part of SOGo * - * Copyright (C) 2007-2010 Inverse inc. + * Copyright (C) 2007-2012 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/UI/Contacts/UIxListEditor.m b/UI/Contacts/UIxListEditor.m index adb04de20..b13677e5f 100644 --- a/UI/Contacts/UIxListEditor.m +++ b/UI/Contacts/UIxListEditor.m @@ -1,6 +1,6 @@ /* UIxListEditor.m - this file is part of SOGo * - * Copyright (C) 2008-2011 Inverse inc. + * Copyright (C) 2008-2012 Inverse inc. * * Author: Wolfgang Sourdeau * Francis Lachapelle diff --git a/UI/MailerUI/Czech.lproj/Localizable.strings b/UI/MailerUI/Czech.lproj/Localizable.strings index 301c74541..0cef2d47d 100644 --- a/UI/MailerUI/Czech.lproj/Localizable.strings +++ b/UI/MailerUI/Czech.lproj/Localizable.strings @@ -11,7 +11,7 @@ "Reply" = "Odpovědět"; "Reply All" = "Odp. všem"; "Print" = "Tisk"; -"Stop" = "Stop"; +"Stop" = "Zastavit"; "Write" = "Napsat"; "Send" = "Odeslat"; @@ -19,6 +19,7 @@ "Attach" = "Přiložit"; "Save" = "Uložit"; "Options" = "Možnosti"; +"Close" = "Zavřít"; "Size" = "Velikost"; /* Tooltips */ @@ -63,8 +64,10 @@ "Shared Account: " = "Sdílený účet: "; /* acls */ -"Default Roles" = "Výchozí oprávnění"; -"User rights for:" = "Uživatelská práva pro:"; +"Access rights to" = "Přístupová práva k"; +"For user" = "Pro uživatele"; + +"Any Authenticated User" = "Všichni ověření uživatelé"; "List and see this folder" = "Prohlížet tuto složku"; "Read mails from this folder" = "Číst maily v této složce"; @@ -104,15 +107,13 @@ "cc" = "Kopie"; "bcc" = "Skrytá kopie"; -"Addressbook" = "Adresář"; - "Edit Draft..." = "Upravit koncept..."; "Load Images" = "Nahrát obrázky"; -"Return Receipt" = "Return Receipt"; -"The sender of this message has asked to be notified when you read this message. Do you with to notify the sender?" = "The sender of this message has asked to be notified when you read this message. Do you with to notify the sender?"; -"Return Receipt (displayed) - %@"= "Return Receipt (displayed) - %@"; -"This is a Return Receipt for the mail that you sent to %@.\n\nNote: This Return Receipt only acknowledges that the message was displayed on the recipient's computer. There is no guarantee that the recipient has read or understood the message contents." = "This is a Return Receipt for the mail that you sent to %@.\n\nNote: This Return Receipt only acknowledges that the message was displayed on the recipient's computer. There is no guarantee that the recipient has read or understood the message contents."; +"Return Receipt" = "Potvrzení o přečtení"; +"The sender of this message has asked to be notified when you read this message. Do you with to notify the sender?" = "Odesílatel této zprávy si přeje být informován o tom, že jste si tuto zprávu přečetli. Chcete odesílateli poslat potvrzení?"; +"Return Receipt (displayed) - %@"= "Potvrzení o přečtení (zobrazeno) - %@"; +"This is a Return Receipt for the mail that you sent to %@.\n\nNote: This Return Receipt only acknowledges that the message was displayed on the recipient's computer. There is no guarantee that the recipient has read or understood the message contents." = "Toto je potvrzení o přečtení ke zprávě, kterou jste poslali pro %@.\n\nPoznámka: Potvrzení o přijetí znamená pouze to, že se zpráva zobrazila na počítači adresáta. Není ale zaručeno, že adresát zprávu četl a porozuměl jejímu obsahu."; "Priority" = "Priorita"; "highest" = "Nejvyšší"; @@ -123,6 +124,9 @@ "This mail is being sent from an unsecure network!" = "Tento mail je odesílán z nezabezpečené sítě!"; +"Address Book:" = "Adresář:"; +"Search For:" = "Hledat:"; + /* Popup "show" */ "all" = "všechny"; @@ -141,8 +145,7 @@ "Date" = "Datum"; "View" = "Zobrazit"; "All" = "Všechny"; -"Unread" = "Nepřečtené"; -"No message" = "No message"; +"No message" = "Žádná zpráva"; "messages" = "zprávy"; "first" = "Nejnovější"; @@ -196,8 +199,8 @@ "Delete Folder" = "Smazat složku"; "Use This Folder For" = "Použít tuto složku pro"; "Get Messages for Account" = "Stáhnout zprávy pro účet"; -"Properties..." = "Properties..."; -"Delegation..." = "Delegation..."; +"Properties..." = "Vlastnosti..."; +"Delegation..." = "Delegování..."; /* Use This Folder menu */ "Sent Messages" = "Odeslané zprávy"; @@ -208,7 +211,6 @@ "Open Message In New Window" = "Otevřít zprávu v novém okně"; "Reply to Sender Only" = "Odpovědět pouze odesílateli"; "Reply to All" = "Odpovědět všem"; -"Forward" = "Přeposlat"; "Edit As New..." = "Upravit jako novou..."; "Move To" = "Přesunout do"; "Copy To" = "Kopírovat do"; @@ -255,11 +257,11 @@ "Please select a message." = "Vyberte zprávu prosím."; "Please select a message to print." = "Zvolte prosím zprávu, kterou chcete tisknout."; "Please select only one message to print." = "Zvolte pouze jednu zprávu, kterou chcete tisknout."; -"The message you have selected doesn't exist anymore." = "The message you have selected doesn't exist anymore."; +"The message you have selected doesn't exist anymore." = "Zpráva, kterou jste zvolili, již neexistuje."; "The folder with name \"%{0}\" could not be created." -= "Složka s názvem \"%{0}\" nemohla být vytvořen."; += "Složka s názvem \"%{0}\" nemohla být vytvořena."; "This folder could not be renamed to \"%{0}\"." = "Tato složka nemohla být přejmenována na \"%{0}\"."; "The folder could not be deleted." @@ -272,20 +274,20 @@ "You need to choose a non-virtual folder!" = "Musíte zvolit ne-virtuální složku!"; "Moving a message into its own folder is impossible!" -= "Je nemožné přesunout zprávu do své vlastní složky!"; += "Zprávu nelze přesunout do své vlastní složky!"; "Copying a message into its own folder is impossible!" -= "Je nemožné zkopírovat zprávu do své vlastní složky!"; += "Zprávu nelze zkopírovat do své vlastní složky!"; /* Message operations */ "The messages could not be moved to the trash folder. Would you like to delete them immediately?" -= "The messages could not be moved to the trash folder. Would you like to delete them immediately?"; += "Zprávy nemohou být přesunuty do koše. Chcete je smazat trvale?"; /* Message editing */ -"error_validationfailed" = "Potvrzení selhalo"; "error_missingsubject" = "Chybí předmět"; "error_missingrecipients" = "Příjemci nebyli specifikováni"; +"Send Anyway" = "Odeslat"; /* Message sending */ -"cannot send message: (smtp) all recipients discarded" = "Cannot send message: all recipients are invalid."; -"cannot send message (smtp) - recipients discarded:" = "Cannot send message. The following addresses are invalid:"; -"cannot send message: (smtp) error when connecting" = "Cannot send message: error when connecting to the SMTP server."; +"cannot send message: (smtp) all recipients discarded" = "Zprávu nelze odeslat: adresy všech příjemců jsou neplatné."; +"cannot send message (smtp) - recipients discarded:" = "Zprávu nelze odeslat: následující adresy jsou neplatné:"; +"cannot send message: (smtp) error when connecting" = "Zprávu nelze odeslat: při spojení se SMTP serverem došlo k chybě."; diff --git a/UI/Scheduler/UIxAttendeesEditor.h b/UI/Scheduler/UIxAttendeesEditor.h index 7aa291a4e..530c33c51 100644 --- a/UI/Scheduler/UIxAttendeesEditor.h +++ b/UI/Scheduler/UIxAttendeesEditor.h @@ -1,6 +1,6 @@ /* UIxAttendeesEditor.h - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2012 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -28,7 +28,6 @@ @interface UIxAttendeesEditor : UIxComponent { NSString *item; -// NSString *zoom; } - (void) setItem: (NSString *) newItem; diff --git a/UI/Scheduler/UIxAttendeesEditor.m b/UI/Scheduler/UIxAttendeesEditor.m index 51be1437e..40379d48e 100644 --- a/UI/Scheduler/UIxAttendeesEditor.m +++ b/UI/Scheduler/UIxAttendeesEditor.m @@ -1,6 +1,6 @@ /* UIxAttendeesEditor.m - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2012 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/UI/Templates/UIxPageFrame.wox b/UI/Templates/UIxPageFrame.wox index 6df36a98e..d81b37700 100644 --- a/UI/Templates/UIxPageFrame.wox +++ b/UI/Templates/UIxPageFrame.wox @@ -130,6 +130,7 @@