merge of '3f8608c82d9c379cc9e0fea6ffe853cc1949a24b'

and '7634b9d503d61b6240548e75e7f872081debc6b4'

Monotone-Parent: 3f8608c82d9c379cc9e0fea6ffe853cc1949a24b
Monotone-Parent: 7634b9d503d61b6240548e75e7f872081debc6b4
Monotone-Revision: a4c7e35d642045e63467a131fae8049524f450e4

Monotone-Author: flachapelle@inverse.ca
Monotone-Date: 2012-06-30T18:20:30
Monotone-Branch: ca.inverse.sogo
maint-2.0.2
Francis Lachapelle 2012-06-30 18:20:30 +00:00
commit 09504ed1cb
26 changed files with 500 additions and 177 deletions

View File

@ -3,6 +3,38 @@
* SoObjects/SOGo/SOGoObject.m (-initWithName:inContainer:): make
sure that "_name" is neither nil nor empty.
2012-06-27 Jean Raby <jraby@inverse.ca>
* SoObjects/Appointments/SOGoAppointmentObject.m
(PUTAction:): detect conflicting event UID and
deny the request accordingly.
2012-06-21 Ludovic Marcotte <lmarcotte@inverse.ca>
* 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 <lmarcotte@inverse.ca>
* SoObjects/Appointments/SOGoAppointmentObject.m
(-_handleResourcesConflicts:forEvent:): We now
handle correctly recurring events overlapping other
recurring events.
2012-06-19 Ludovic Marcotte <lmarcotte@inverse.ca>
* 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 <wsourdeau@inverse.ca>
* SoObjects/SOGo/SOGoGroup.m

12
NEWS
View File

@ -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

View File

@ -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)");

View File

@ -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}\".";
"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}.";

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -70,4 +70,6 @@
SOGoReminderEnabled = YES;
SOGoRemindWithASound = YES;
SOGoSearchMinimumWordLength = 2;
}

View File

@ -77,6 +77,8 @@
- (BOOL) hideSystemEMail;
- (int) searchMinimumWordLength;
@end
#endif /* SOGODOMAINDEFAULTS_H */

View File

@ -319,4 +319,9 @@
return [self boolForKey: @"SOGoHideSystemEMail"];
}
- (int) searchMinimumWordLength
{
return [self integerForKey: @"SOGoSearchMinimumWordLength"];
}
@end

View File

@ -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

View File

@ -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);

View File

@ -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 */

View File

@ -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.

View File

@ -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 <wsourdeau@inverse.ca>
*

View File

@ -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 <wsourdeau@inverse.ca>
* Francis Lachapelle <flachapelle@inverse.ca>

View File

@ -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..." = "Delegoní...";
/* 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ě.";

View File

@ -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 <wsourdeau@inverse.ca>
*
@ -28,7 +28,6 @@
@interface UIxAttendeesEditor : UIxComponent
{
NSString *item;
// NSString *zoom;
}
- (void) setItem: (NSString *) newItem;

View File

@ -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 <wsourdeau@inverse.ca>
*

View File

@ -130,6 +130,7 @@
<script type="text/javascript">
var ApplicationBaseURL = '<var:string value="modulePath"/>';
var ResourcesURL = '<var:string value="applicationPath"/>.woa/WebServerResources';
var minimumSearchLength = <var:string value="minimumSearchLength"/>;
<var:if condition="isSuperUser"
>var IsSuperUser = true;
</var:if>

View File

@ -125,7 +125,7 @@ var SOGoAutoCompletionInterface = {
document.contactLookupAjaxRequest.aborted = true;
document.contactLookupAjaxRequest.abort();
}
if (input.value.trim().length > 2) {
if (input.value.trim().length > minimumSearchLength) {
var urlstr = UserFolderURL + "Contacts/";
if (input.addressBook)
urlstr += input.addressBook + "/contact";

View File

@ -177,7 +177,7 @@ function onContactKeydown(event) {
function performSearch(input) {
// Perform address completion
if (!input.value.blank()) {
if (input.value.trim().length > minimumSearchLength) {
var urlstr = (UserFolderURL
+ "Contacts/allContactSearch?excludeGroups=1&search="
+ encodeURIComponent(input.value));

View File

@ -1,8 +1,6 @@
var d;
function onSearchFormSubmit() {
startAnimation($("pageContent"), $("filterPanel"));
var searchValue = $("searchValue");
var encodedValue = encodeURI(searchValue.value);
@ -16,8 +14,11 @@ function onSearchFormSubmit() {
document.userFoldersRequest.aborted = true;
document.userFoldersRequest.abort();
}
document.userFoldersRequest
= triggerAjaxRequest(url, usersSearchCallback);
if (encodedValue.trim().length > minimumSearchLength) {
startAnimation($("pageContent"), $("filterPanel"));
document.userFoldersRequest
= triggerAjaxRequest(url, usersSearchCallback);
}
}
return false;

View File

@ -1,7 +1,7 @@
/* generic.js - this file is part of SOGo
Copyright (C) 2005 SKYRIX Software AG
Copyright (C) 2006-2011 Inverse
Copyright (C) 2006-2012 Inverse
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
@ -1143,7 +1143,7 @@ function onSearchFormSubmit(event) {
if (searchValue.value != searchValue.ghostPhrase
&& (searchValue.value != searchValue.lastSearch
|| searchValue.value.strip().length > 0)) {
&& searchValue.value.strip().length > minimumSearchLength)) {
search["criteria"] = searchCriteria.value;
search["value"] = searchValue.value;
searchValue.lastSearch = searchValue.value;

View File

@ -15,7 +15,7 @@ Group: Productivity/Groupware
Source: SOGo-%{sogo_version}.tar.gz
Prefix: /usr
AutoReqProv: off
Requires: gnustep-base >= 1.23, sope%{sope_major_version}%{sope_minor_version}-core, httpd, sope%{sope_major_version}%{sope_minor_version}-core, sope%{sope_major_version}%{sope_minor_version}-appserver, sope%{sope_major_version}%{sope_minor_version}-ldap, sope%{sope_major_version}%{sope_minor_version}-cards >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-gdl1-contentstore >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-sbjson, memcached, libmemcached
Requires: gnustep-base >= 1.23, sope%{sope_major_version}%{sope_minor_version}-core, httpd, sope%{sope_major_version}%{sope_minor_version}-core, sope%{sope_major_version}%{sope_minor_version}-appserver, sope%{sope_major_version}%{sope_minor_version}-ldap, sope%{sope_major_version}%{sope_minor_version}-cards >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-gdl1-contentstore >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-sbjson, libmemcached
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}
BuildRequires: gcc-objc gnustep-base gnustep-make sope%{sope_major_version}%{sope_minor_version}-appserver-devel sope%{sope_major_version}%{sope_minor_version}-core-devel sope%{sope_major_version}%{sope_minor_version}-ldap-devel sope%{sope_major_version}%{sope_minor_version}-mime-devel sope%{sope_major_version}%{sope_minor_version}-xml-devel sope%{sope_major_version}%{sope_minor_version}-gdl1-devel sope%{sope_major_version}%{sope_minor_version}-sbjson-devel libmemcached-devel %{?oc_build_depends}