2 Copyright (C) 2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
23 #include <SOGoUI/UIxComponent.h>
29 @class iCalRecurrenceRule;
30 @class SOGoAppointment;
32 @interface UIxAppointmentEditor : UIxComponent
38 /* individual values */
39 NSCalendarDate *startDate;
40 NSCalendarDate *endDate;
41 NSCalendarDate *cycleUntilDate;
45 NSArray *participants; /* array of iCalPerson's */
46 NSArray *resources; /* array of iCalPerson's */
49 NSString *accessClass;
50 BOOL isPrivate; /* default: NO */
51 BOOL checkForConflicts; /* default: NO */
55 - (NSString *)iCalStringTemplate;
56 - (NSString *)iCalString;
58 - (void)setIsPrivate:(BOOL)_yn;
59 - (void)setAccessClass:(NSString *)_class;
61 - (void)setCheckForConflicts:(BOOL)_checkForConflicts;
62 - (BOOL)checkForConflicts;
65 - (iCalRecurrenceRule *)rrule;
66 - (void)adjustCycleControlsForRRule:(iCalRecurrenceRule *)_rrule;
67 - (NSDictionary *)cycleMatchingRRule:(iCalRecurrenceRule *)_rrule;
69 - (NSString *)_completeURIForMethod:(NSString *)_method;
71 - (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values
72 treatAsResource:(BOOL)_isResource;
74 - (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters;
75 - (NSString *)iCalParticipantsStringFromQueryParameters;
76 - (NSString *)iCalResourcesStringFromQueryParameters;
77 - (NSString *)iCalStringFromQueryParameter:(NSString *)_qp
78 format:(NSString *)_format;
79 - (NSString *)iCalOrganizerString;
81 - (id)acceptOrDeclineAction:(BOOL)_accept;
86 #include <NGiCal/NGiCal.h>
87 #include <NGExtensions/NGCalendarDateRange.h>
88 #include <SOGoUI/SOGoDateFormatter.h>
89 #include <SOGo/SOGoAppointment.h>
90 #include <SOGo/AgenorUserManager.h>
91 #include <Appointments/SOGoAppointmentFolder.h>
92 #include <Appointments/SOGoAppointmentObject.h>
93 #include "iCalPerson+UIx.h"
94 #include "UIxComponent+Agenor.h"
96 @interface iCalRecurrenceRule (SOGoExtensions)
97 - (NSString *)cycleRepresentationForSOGo;
100 @interface NSDate(UsedPrivates)
101 - (NSString *)icalString; // TODO: this is in NGiCal
104 @implementation UIxAppointmentEditor
109 [self setIsPrivate:NO];
110 [self setCheckForConflicts:NO];
116 [self->iCalString release];
117 [self->errorText release];
118 [self->item release];
120 [self->startDate release];
121 [self->endDate release];
122 [self->cycleUntilDate release];
123 [self->title release];
124 [self->location release];
125 [self->comment release];
126 [self->participants release];
127 [self->resources release];
128 [self->priority release];
129 [self->categories release];
130 [self->accessClass release];
131 [self->cycle release];
137 - (void)setItem:(id)_item {
138 ASSIGN(self->item, _item);
144 - (void)setErrorText:(NSString *)_txt {
145 ASSIGNCOPY(self->errorText, _txt);
147 - (NSString *)errorText {
148 return self->errorText;
150 - (BOOL)hasErrorText {
151 return [self->errorText length] > 0 ? YES : NO;
154 - (NSFormatter *)titleDateFormatter {
155 SOGoDateFormatter *fmt;
157 fmt = [[[SOGoDateFormatter alloc] initWithLocale:[self locale]] autorelease];
158 [fmt setFullWeekdayNameAndDetails];
162 - (void)setAptStartDate:(NSCalendarDate *)_date {
163 ASSIGN(self->startDate, _date);
165 - (NSCalendarDate *)aptStartDate {
166 return self->startDate;
168 - (void)setAptEndDate:(NSCalendarDate *)_date {
169 ASSIGN(self->endDate, _date);
171 - (NSCalendarDate *)aptEndDate {
172 return self->endDate;
175 - (void)setTitle:(NSString *)_value {
176 ASSIGNCOPY(self->title, _value);
178 - (NSString *)title {
181 - (void)setLocation:(NSString *)_value {
182 ASSIGNCOPY(self->location, _value);
184 - (NSString *)location {
185 return self->location;
187 - (void)setComment:(NSString *)_value {
188 ASSIGNCOPY(self->comment, _value);
190 - (NSString *)comment {
191 return self->comment;
194 - (void)setParticipants:(NSArray *)_parts {
195 ASSIGN(self->participants, _parts);
197 - (NSArray *)participants {
198 return self->participants;
200 - (void)setResources:(NSArray *)_res {
201 ASSIGN(self->resources, _res);
203 - (NSArray *)resources {
204 return self->resources;
209 - (NSArray *)priorities {
214 static NSArray *priorities = nil;
217 priorities = [[NSArray arrayWithObjects:@"0", @"5", @"1", nil] retain];
221 - (NSString *)itemPriorityText {
224 key = [NSString stringWithFormat:@"prio_%@", self->item];
225 return [self labelForKey:key];
228 - (void)setPriority:(NSString *)_priority {
229 ASSIGN(self->priority, _priority);
231 - (NSString *)priority {
232 return self->priority;
238 - (NSArray *)categoryItems {
239 // TODO: make this configurable?
241 Tasks categories will be modified as follow :
242 – by default (a simple logo or no logo at all),
249 static NSArray *categoryItems = nil;
251 if (!categoryItems) {
252 categoryItems = [[NSArray arrayWithObjects:@"APPOINTMENT",
259 return categoryItems;
262 - (NSString *)itemCategoryText {
263 return [self labelForKey:self->item];
266 - (void)setCategories:(NSArray *)_categories {
267 ASSIGN(self->categories, _categories);
269 - (NSArray *)categories {
270 return self->categories;
276 - (NSArray *)accessClassItems {
277 static NSArray classItems = nil;
280 return [[NSArray arrayWithObjects:@"PUBLIC", @"PRIVATE", nil] retain];
286 - (void)setAccessClass:(NSString *)_class {
287 ASSIGN(self->accessClass, _class);
289 - (NSString *)accessClass {
290 return self->accessClass;
293 - (void)setIsPrivate:(BOOL)_yn {
295 [self setAccessClass:@"PRIVATE"];
297 [self setAccessClass:@"PUBLIC"];
298 self->isPrivate = _yn;
301 return self->isPrivate;
304 - (void)setCheckForConflicts:(BOOL)_checkForConflicts {
305 self->checkForConflicts = _checkForConflicts;
307 - (BOOL)checkForConflicts {
308 return self->checkForConflicts;
311 - (NSArray *)cycles {
312 static NSArray *cycles = nil;
318 bundle = [NSBundle bundleForClass:[self class]];
319 path = [bundle pathForResource:@"cycles" ofType:@"plist"];
320 NSAssert(path != nil, @"Cannot find cycles.plist!");
321 cycles = [[NSArray arrayWithContentsOfFile:path] retain];
322 NSAssert(cycles != nil, @"Cannot instantiate cycles from cycles.plist!");
327 - (void)setCycle:(NSDictionary *)_cycle {
328 ASSIGN(self->cycle, _cycle);
330 - (NSDictionary *)cycle {
334 [self debugWithFormat:@"cycle: %@", self->cycle];
335 if (![self->cycle objectForKey:@"rule"])
339 - (NSString *)cycleLabel {
342 key = [self->item objectForKey:@"label"];
343 return [self labelForKey:key];
346 - (iCalRecurrenceRule *)rrule {
348 iCalRecurrenceRule *rule;
350 if (![self hasCycle])
352 ruleRep = [self->cycle objectForKey:@"rule"];
353 rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:ruleRep];
355 if (self->cycleUntilDate)
356 [rule setUntilDate:self->cycleUntilDate];
360 - (void)adjustCycleControlsForRRule:(iCalRecurrenceRule *)_rrule {
363 c = [self cycleMatchingRRule:_rrule];
366 [self->cycleUntilDate release];
367 self->cycleUntilDate = [[_rrule untilDate] copy];
368 [self->cycleUntilDate setTimeZone:[self viewTimeZone]];
372 This method is necessary, because we have a fixed sets of cycles in the UI.
373 The model is able to represent arbitrary rules, however.
374 There SHOULD be a different UI, similar to iCal.app, to allow modelling
375 of more complex rules.
377 This method obviously cannot map all existing rules back to the fixed list
378 in cycles.plist. This should be fixed in a future version when interop
379 becomes more important.
381 - (NSDictionary *)cycleMatchingRRule:(iCalRecurrenceRule *)_rrule {
387 return [[self cycles] objectAtIndex:0];
389 cycleRep = [_rrule cycleRepresentationForSOGo];
390 cycles = [self cycles];
391 count = [cycles count];
392 for (i = 1; i < count; i++) {
396 c = [cycles objectAtIndex:i];
397 cr = [c objectForKey:@"rule"];
398 if ([cr isEqualToString:cycleRep])
401 [self warnWithFormat:@"No default cycle for rrule found! -> %@", _rrule];
408 - (NSString *)transparency {
409 return @"TRANSPARENT";
415 - (void)setICalString:(NSString *)_s {
416 ASSIGNCOPY(self->iCalString, _s);
418 - (NSString *)iCalString {
419 return self->iCalString;
422 - (NSString *)iCalStringTemplate {
423 static NSString *iCalStringTemplate = \
424 @"BEGIN:VCALENDAR\r\n"
425 @"METHOD:REQUEST\r\n"
426 @"PRODID:OpenGroupware.org SOGo 0.9\r\n"
431 @"STATUS:CONFIRMED\r\n" /* confirmed by default */
438 @"%@" /* organizer */
439 @"%@" /* participants and resources */
443 NSCalendarDate *lStartDate, *lEndDate;
444 NSString *template, *s;
447 s = [self queryParameterForKey:@"dur"];
448 if(s && [s length] > 0) {
449 minutes = [s intValue];
454 lStartDate = [self selectedDate];
455 lEndDate = [lStartDate dateByAddingYears:0 months:0 days:0
456 hours:0 minutes:minutes seconds:0];
458 s = [self iCalParticipantsAndResourcesStringFromQueryParameters];
459 template = [NSString stringWithFormat:iCalStringTemplate,
460 [[self clientObject] nameInContainer],
461 [[NSCalendarDate date] icalString],
462 [lStartDate icalString],
463 [lEndDate icalString],
465 [self iCalOrganizerString],
470 - (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters {
473 s = [self iCalParticipantsStringFromQueryParameters];
474 return [s stringByAppendingString:
475 [self iCalResourcesStringFromQueryParameters]];
478 - (NSString *)iCalParticipantsStringFromQueryParameters {
479 static NSString *iCalParticipantString = \
480 @"ATTENDEE;ROLE=REQ-PARTICIPANT;CN=\"%@\":mailto:%@\r\n";
482 return [self iCalStringFromQueryParameter:@"ps"
483 format:iCalParticipantString];
486 - (NSString *)iCalResourcesStringFromQueryParameters {
487 static NSString *iCalResourceString = \
488 @"ATTENDEE;ROLE=NON-PARTICIPANT;CN=\"%@\":mailto:%@\r\n";
490 return [self iCalStringFromQueryParameter:@"rs"
491 format:iCalResourceString];
494 - (NSString *)iCalStringFromQueryParameter:(NSString *)_qp
495 format:(NSString *)_format
497 AgenorUserManager *um;
498 NSMutableString *iCalRep;
501 um = [AgenorUserManager sharedUserManager];
502 iCalRep = (NSMutableString *)[NSMutableString string];
503 s = [self queryParameterForKey:_qp];
504 if(s && [s length] > 0) {
508 es = [s componentsSeparatedByString:@","];
510 for(i = 0; i < count; i++) {
511 NSString *email, *cn;
513 email = [es objectAtIndex:i];
514 cn = [um getCNForUID:[um getUIDForEmail:email]];
515 [iCalRep appendFormat:_format, cn, email];
521 - (NSString *)iCalOrganizerString {
522 static NSString *fmt = @"ORGANIZER;CN=\"%@\":mailto:%@\r\n";
523 return [NSString stringWithFormat:fmt,
525 [self emailForUser]];
529 - (iCalPerson *)getOrganizer {
533 emailProp = [@"mailto:" stringByAppendingString:[self emailForUser]];
534 p = [[[iCalPerson alloc] init] autorelease];
535 [p setEmail:emailProp];
536 [p setCn:[self cnForUser]];
544 - (NSString *)_completeURIForMethod:(NSString *)_method {
548 uri = [[[self context] request] uri];
550 /* first: identify query parameters */
551 r = [uri rangeOfString:@"?" options:NSBackwardsSearch];
553 uri = [uri substringToIndex:r.location];
555 /* next: append trailing slash */
556 if (![uri hasSuffix:@"/"])
557 uri = [uri stringByAppendingString:@"/"];
559 /* next: append method */
560 uri = [uri stringByAppendingString:_method];
562 /* next: append query parameters */
563 return [self completeHrefForMethod:uri];
570 This method creates a unique ID and redirects to the "edit" method on the
572 It is actually a folder method and should be defined on the folder.
574 Note: 'clientObject' is the SOGoAppointmentFolder!
575 Update: remember that there are group folders as well.
577 NSString *uri, *objectId, *method;
579 objectId = [NSClassFromString(@"SOGoAppointmentFolder")
580 globallyUniqueObjectId];
581 if ([objectId length] == 0) {
582 return [NSException exceptionWithHTTPStatus:500 /* Internal Error */
583 reason:@"could not create a unique ID"];
586 method = [NSString stringWithFormat:@"Calendar/%@/edit", objectId];
587 method = [[self userFolderPath] stringByAppendingPathComponent:method];
589 /* add all current calendarUIDs as default participants */
590 if ([[self clientObject] respondsToSelector:@selector(calendarUIDs)]) {
591 AgenorUserManager *um;
593 NSMutableArray *emails;
597 um = [AgenorUserManager sharedUserManager];
598 uids = [[self clientObject] calendarUIDs];
599 count = [uids count];
600 emails = [NSMutableArray arrayWithCapacity:count];
602 for (i = 0; i < count; i++) {
605 email = [um getEmailForUID:[uids objectAtIndex:i]];
607 [emails addObject:email];
609 ps = [emails componentsJoinedByString:@","];
610 [self setQueryParameter:ps forKey:@"ps"];
612 uri = [self completeHrefForMethod:method];
613 return [self redirectToLocation:uri];
618 /* returned dates are in GMT */
619 - (NSCalendarDate *)_dateFromString:(NSString *)_str {
620 NSCalendarDate *date;
622 date = [NSCalendarDate dateWithString:_str
623 calendarFormat:@"%Y-%m-%d %H:%M %Z"];
624 [date setTimeZone:[self backendTimeZone]];
628 - (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values
629 treatAsResource:(BOOL)_isResource
632 NSMutableArray *result;
634 count = [_values count];
635 result = [[NSMutableArray alloc] initWithCapacity:count];
636 for (i = 0; i < count; i++) {
637 NSString *pString, *email, *cn;
641 pString = [_values objectAtIndex:i];
642 if ([pString length] == 0)
645 /* delimiter between email and cn */
646 r = [pString rangeOfString:@";"];
648 email = [pString substringToIndex:r.location];
649 cn = (r.location + 1 < [pString length])
650 ? [pString substringFromIndex:r.location + 1]
659 AgenorUserManager *um = [AgenorUserManager sharedUserManager];
660 cn = [um getCNForUID:[um getUIDForEmail:email]];
663 p = [[iCalPerson alloc] init];
664 [p setEmail:[@"mailto:" stringByAppendingString:email]];
665 if ([cn isNotNull]) [p setCn:cn];
667 /* see RFC2445, sect. 4.2.16 for details */
668 [p setRole:_isResource ? @"NON-PARTICIPANT" : @"REQ-PARTICIPANT"];
669 [result addObject:p];
672 return [result autorelease];
675 - (BOOL)isWriteableClientObject {
676 return [[self clientObject]
677 respondsToSelector:@selector(saveContentString:)];
680 - (void)loadValuesFromAppointment:(SOGoAppointment *)_appointment {
682 iCalRecurrenceRule *rrule;
684 if ((self->startDate = [[_appointment startDate] copy]) == nil)
685 self->startDate = [[[NSCalendarDate date] hour:11 minute:0] copy];
686 if ((self->endDate = [[_appointment endDate] copy]) == nil) {
688 [[self->startDate hour:[self->startDate hourOfDay] + 1 minute:0] copy];
690 [self->startDate setTimeZone:[self viewTimeZone]];
691 [self->endDate setTimeZone:[self viewTimeZone]];
693 self->title = [[_appointment summary] copy];
694 self->location = [[_appointment location] copy];
695 self->comment = [[_appointment comment] copy];
696 self->priority = [[_appointment priority] copy];
697 self->categories = [[_appointment categories] retain];
698 self->participants = [[_appointment participants] retain];
699 self->resources = [[_appointment resources] retain];
701 s = [_appointment accessClass];
702 if(!s || [s isEqualToString:@"PUBLIC"])
703 [self setIsPrivate:NO];
705 [self setIsPrivate:YES]; /* we're possibly loosing information here */
708 rrule = [_appointment recurrenceRule];
709 [self adjustCycleControlsForRRule:rrule];
712 - (void)saveValuesIntoAppointment:(SOGoAppointment *)_appointment {
713 /* merge in form values */
714 NSArray *attendees, *lResources;
716 [_appointment setStartDate:[self aptStartDate]];
717 [_appointment setEndDate:[self aptEndDate]];
719 [_appointment setSummary:[self title]];
720 [_appointment setLocation:[self location]];
721 [_appointment setComment:[self comment]];
722 [_appointment setPriority:[self priority]];
723 [_appointment setCategories:[self categories]];
725 [_appointment setAccessClass:[self accessClass]];
726 [_appointment setTransparency:[self transparency]];
728 attendees = [self participants];
729 lResources = [self resources];
730 if ([lResources count] > 0) {
731 attendees = ([attendees count] > 0)
732 ? [attendees arrayByAddingObjectsFromArray:lResources]
735 [_appointment setAttendees:attendees];
738 [_appointment setRecurrenceRule:[self rrule]];
741 - (void)loadValuesFromICalString:(NSString *)_ical {
742 SOGoAppointment *apt;
744 apt = [[SOGoAppointment alloc] initWithICalString:_ical];
745 [self loadValuesFromAppointment:apt];
749 /* contact editor compatibility */
751 - (void)setContentString:(NSString *)_s {
752 [self setICalString:_s];
754 - (NSString *)contentStringTemplate {
755 return [self iCalStringTemplate];
758 - (void)loadValuesFromContentString:(NSString *)_s {
759 [self loadValuesFromICalString:_s];
768 owner = [[self clientObject] ownerInContext:[self context]];
771 return [owner isEqualToString:[[self user] login]];
774 - (BOOL)canAccessApt {
775 return [self isMyApt];
779 return [self isMyApt];
783 /* conflict management */
785 - (BOOL)containsConflict:(SOGoAppointment *)_apt {
786 NSArray *attendees, *uids;
787 SOGoAppointmentFolder *groupCalendar;
792 [self logWithFormat:@"search from %@ to %@",
793 [_apt startDate], [_apt endDate]];
795 folder = [[self clientObject] container];
796 attendees = [_apt attendees];
797 uids = [folder uidsFromICalPersons:attendees];
798 if ([uids count] == 0) {
799 [self logWithFormat:@"Note: no UIDs selected."];
803 groupCalendar = [folder lookupGroupCalendarFolderForUIDs:uids
804 inContext:[self context]];
805 [self debugWithFormat:@"group calendar: %@", groupCalendar];
807 if (![groupCalendar respondsToSelector:@selector(fetchFreebusyInfosFrom:to:)]) {
808 [self errorWithFormat:@"invalid folder to run freebusy query on!"];
812 infos = [groupCalendar fetchFreebusyInfosFrom:[_apt startDate]
814 [self debugWithFormat:@" process: %d events", [infos count]];
816 ranges = [infos arrayByCreatingDateRangesFromObjectsWithStartDateKey:@"startDate"
817 andEndDateKey:@"endDate"];
818 ranges = [ranges arrayByCompactingContainedDateRanges];
819 [self debugWithFormat:@" blocked ranges: %@", ranges];
821 return [ranges count] != 0 ? YES : NO;
827 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
832 /* for testing only */
834 SOGoAppointment *apt;
837 req = [[self context] request];
838 apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]];
839 [self saveValuesIntoAppointment:apt];
840 content = [apt iCalString];
841 [self logWithFormat:@"%s -- iCal:\n%@",
848 - (id)defaultAction {
851 /* load iCalendar file */
853 // TODO: can't we use [clientObject contentAsString]?
854 ical = [[self clientObject] valueForKey:@"iCalString"];
855 if ([ical length] == 0) /* a new appointment */
856 ical = [self contentStringTemplate];
858 [self setContentString:ical];
859 [self loadValuesFromContentString:ical];
865 SOGoAppointment *apt;
870 if (![self isWriteableClientObject]) {
871 /* return 400 == Bad Request */
872 return [NSException exceptionWithHTTPStatus:400
873 reason:@"method cannot be invoked on "
874 @"the specified object"];
877 apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]];
881 s = [self labelForKey:@"Invalid iCal data!"];
882 [self setErrorText:s];
886 [self saveValuesIntoAppointment:apt];
887 p = [apt findParticipantWithEmail:[self emailForUser]];
889 [p setParticipationStatus:iCalPersonPartStatAccepted];
892 if ([self checkForConflicts]) {
893 if ([self containsConflict:apt]) {
896 s = [self labelForKey:@"Conflicts found!"];
897 [self setErrorText:s];
902 content = [apt iCalString];
903 [apt release]; apt = nil;
905 if (content == nil) {
908 s = [self labelForKey:@"Could not create iCal data!"];
909 [self setErrorText:s];
913 ex = [[self clientObject] saveContentString:content];
915 [self setErrorText:[ex reason]];
919 return [self redirectToLocation:[self _completeURIForMethod:@".."]];
923 return [self acceptOrDeclineAction:YES];
926 - (id)declineAction {
927 return [self acceptOrDeclineAction:NO];
930 - (id)acceptOrDeclineAction:(BOOL)_accept {
931 SOGoAppointment *apt;
933 NSString *iCal, *content;
936 if (![self isWriteableClientObject]) {
937 /* 400 == Bad Request */
938 return [NSException exceptionWithHTTPStatus:400
939 reason:@"method cannot be invoked on "
940 @"the specified object"];
942 iCal = [[self clientObject] valueForKey:@"iCalString"];
943 apt = [[SOGoAppointment alloc] initWithICalString:iCal];
945 /* 500 == Internal Server Error */
946 return [NSException exceptionWithHTTPStatus:500
947 reason:@"unable to parse appointment"];
950 p = [apt findParticipantWithEmail:[self emailForUser]];
952 /* 404 == Not found */
953 return [NSException exceptionWithHTTPStatus:404
954 reason:@"user does not participate in this "
958 [p setParticipationStatus:iCalPersonPartStatAccepted];
960 [p setParticipationStatus:iCalPersonPartStatDeclined];
962 content = [apt iCalString];
965 if (content == nil) {
966 /* 500 == Internal Server Error */
967 return [NSException exceptionWithHTTPStatus:500
968 reason:@"Could not create iCalendar data ..."];
971 ex = [[self clientObject] saveContentString:content];
973 /* 500 == Internal Server Error */
974 return [NSException exceptionWithHTTPStatus:500
978 return [self redirectToLocation:[self _completeURIForMethod:@".."]];
981 @end /* UIxAppointmentEditor */
985 This is a pretty ugly (unfortunately necessary) hack to map our limited
986 set of recurrence rules back to the popup list
988 @interface iCalRecurrenceRule (UsedPrivates)
990 - (NSString *)byDayList;
991 @end /* iCalRecurrenceRule (UsedPrivates) */
993 @implementation iCalRecurrenceRule (SOGoExtensions)
995 - (NSString *)cycleRepresentationForSOGo {
998 s = [NSMutableString stringWithCapacity:20];
999 [s appendString:@"FREQ="];
1000 [s appendString:[self freq]];
1001 if ([self repeatInterval] != 1) {
1002 [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
1004 if (self->byDay.mask != 0) {
1005 [s appendString:@";BYDAY="];
1006 [s appendString:[self byDayList]];
1011 @end /* iCalRecurrenceRule (SOGoExtensions) */