From 1c44c545020abd0d655f8a115a536f4c01b75350 Mon Sep 17 00:00:00 2001 From: Wolfgang Sourdeau Date: Wed, 11 Oct 2006 18:15:29 +0000 Subject: [PATCH] Monotone-Parent: 8f5116023daecdd209e3db884fc339164a4ff419 Monotone-Revision: 284fc46fc7a7b10eb3aecddeb12116108147b6d1 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2006-10-11T18:15:29 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 7 + UI/Scheduler/UIxTaskEditor.m | 1100 ++++++++++++++++++++++++++++++++ UI/Scheduler/UIxTaskProposal.m | 488 ++++++++++++++ UI/Scheduler/UIxTaskView.h | 40 ++ UI/Scheduler/UIxTaskView.m | 296 +++++++++ 5 files changed, 1931 insertions(+) create mode 100644 UI/Scheduler/UIxTaskEditor.m create mode 100644 UI/Scheduler/UIxTaskProposal.m create mode 100644 UI/Scheduler/UIxTaskView.h create mode 100644 UI/Scheduler/UIxTaskView.m diff --git a/ChangeLog b/ChangeLog index ffe3990a3..c44ce423b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,12 @@ 2006-10-11 Wolfgang Sourdeau + * UI/Scheduler/UIxTaskProposal.[hm], + UI/Scheduler/UIxTaskView.[hm], UI/Scheduler/UIxTaskEditor.[hm]: + clones of the UIxAppointment* classes for the handling of tasks. + + * UI/WebServerResources/UIxTaskEditor.js: clone of + UIxAppointmentEditor adapted for the handling of tasks. + * UI/WebServerResources/SchedulerUI.js: added support for tasks. Scroll the daily view to the appropriate hour when an appointment is selected in the appointments list. diff --git a/UI/Scheduler/UIxTaskEditor.m b/UI/Scheduler/UIxTaskEditor.m new file mode 100644 index 000000000..cda628465 --- /dev/null +++ b/UI/Scheduler/UIxTaskEditor.m @@ -0,0 +1,1100 @@ +/* + Copyright (C) 2004-2005 SKYRIX Software AG + + This file is part of OpenGroupware.org. + + OGo 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 + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with OGo; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ + +#import +#import + +#import +#import + +/* TODO: CLEAN UP */ + +@class NSString; +@class iCalPerson; +@class iCalRecurrenceRule; + +@interface UIxTaskEditor : UIxComponent +{ + NSString *iCalString; + NSString *errorText; + id item; + + /* individual values */ + NSCalendarDate *startDate; + NSCalendarDate *dueDate; + NSCalendarDate *cycleUntilDate; + NSString *title; + NSString *location; + NSString *comment; + iCalPerson *organizer; + NSArray *participants; /* array of iCalPerson's */ + NSArray *resources; /* array of iCalPerson's */ + NSString *priority; + NSArray *categories; + NSString *accessClass; + BOOL isPrivate; /* default: NO */ + BOOL checkForConflicts; /* default: NO */ + NSDictionary *cycle; + NSString *cycleEnd; +} + +- (NSString *)iCalStringTemplate; +- (NSString *)iCalString; + +- (void)setIsPrivate:(BOOL)_yn; +- (void)setAccessClass:(NSString *)_class; + +- (void)setCheckForConflicts:(BOOL)_checkForConflicts; +- (BOOL)checkForConflicts; + +- (BOOL)hasCycle; +- (iCalRecurrenceRule *)rrule; +- (void)adjustCycleControlsForRRule:(iCalRecurrenceRule *)_rrule; +- (NSDictionary *)cycleMatchingRRule:(iCalRecurrenceRule *)_rrule; + +- (BOOL)isCycleEndUntil; +- (void)setIsCycleEndUntil; +- (void)setIsCycleEndNever; + +- (NSString *)_completeURIForMethod:(NSString *)_method; + +- (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values + treatAsResource:(BOOL)_isResource; + +- (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters; +- (NSString *)iCalParticipantsStringFromQueryParameters; +- (NSString *)iCalResourcesStringFromQueryParameters; +- (NSString *)iCalStringFromQueryParameter:(NSString *)_qp + format:(NSString *)_format; +- (NSString *)iCalOrganizerString; + +- (id)acceptOrDeclineAction:(BOOL)_accept; + +@end + +#import "common.h" +#import +#import +#import +#import +#import +#import +#import "UIxComponent+Agenor.h" + +@implementation UIxTaskEditor + ++ (int)version { + return [super version] + 0 /* v2 */; +} + ++ (void)initialize { + NSAssert2([super version] == 2, + @"invalid superclass (%@) version %i !", + NSStringFromClass([self superclass]), [super version]); +} + +- (id)init { + self = [super init]; + if(self) { + [self setIsPrivate:NO]; + [self setCheckForConflicts:NO]; + [self setIsCycleEndNever]; + } + return self; +} + +- (void)dealloc { + [iCalString release]; + [errorText release]; + [item release]; + + [startDate release]; + [dueDate release]; + [cycleUntilDate release]; + [title release]; + [location release]; + [organizer release]; + [comment release]; + [participants release]; + [resources release]; + [priority release]; + [categories release]; + [accessClass release]; + [cycle release]; + [cycleEnd release]; + [super dealloc]; +} + +/* accessors */ + +- (void)setItem:(id)_item { + ASSIGN(item, _item); +} +- (id)item { + return item; +} + +- (void)setErrorText:(NSString *)_txt { + ASSIGNCOPY(errorText, _txt); +} +- (NSString *)errorText { + return errorText; +} +- (BOOL)hasErrorText { + return [errorText length] > 0 ? YES : NO; +} + +- (NSFormatter *)titleDateFormatter { + SOGoDateFormatter *fmt; + + fmt = [[[SOGoDateFormatter alloc] initWithLocale:[self locale]] autorelease]; + [fmt setFullWeekdayNameAndDetails]; + return fmt; +} + +- (void)setTaskStartDate:(NSCalendarDate *)_date { + ASSIGN(startDate, _date); +} +- (NSCalendarDate *)taskStartDate { + return startDate; +} +- (void)setTaskDueDate:(NSCalendarDate *)_date { + ASSIGN(dueDate, _date); +} +- (NSCalendarDate *)taskDueDate { + return dueDate; +} + +- (void)setTitle:(NSString *)_value { + ASSIGNCOPY(title, _value); +} +- (NSString *)title { + return title; +} +- (void)setLocation:(NSString *)_value { + ASSIGNCOPY(location, _value); +} +- (NSString *)location { + return location; +} +- (void)setComment:(NSString *)_value { + ASSIGNCOPY(comment, _value); +} +- (NSString *)comment { + return comment; +} + +- (void)setParticipants:(NSArray *)_parts { + ASSIGN(participants, _parts); +} +- (NSArray *)participants { + return participants; +} +- (void)setResources:(NSArray *)_res { + ASSIGN(resources, _res); +} +- (NSArray *)resources { + return resources; +} + +/* priorities */ + +- (NSArray *)priorities { + /* 0 == undefined + 5 == normal + 1 == high + */ + static NSArray *priorities = nil; + + if (!priorities) + priorities = [[NSArray arrayWithObjects:@"0", @"5", @"1", nil] retain]; + return priorities; +} + +- (NSString *)itemPriorityText { + NSString *key; + + key = [NSString stringWithFormat:@"prio_%@", item]; + return [self labelForKey:key]; +} + +- (void)setPriority:(NSString *)_priority { + ASSIGN(priority, _priority); +} +- (NSString *)priority { + return priority; +} + + +/* categories */ + +- (NSArray *)categoryItems { + // TODO: make this configurable? + /* + Tasks categories will be modified as follow : + – by default (a simple logo or no logo at all), + – task, + – outside, + – meeting, + – holidays, + – phone. + */ + static NSArray *categoryItems = nil; + + if (!categoryItems) { + categoryItems = [[NSArray arrayWithObjects:@"TASK", + @"NOT IN OFFICE", + @"MEETING", + @"HOLIDAY", + @"PHONE CALL", + nil] retain]; + } + return categoryItems; +} + +- (NSString *) itemCategoryText { + return [[self labelForKey: item] stringByEscapingHTMLString]; +} + +- (void)setCategories:(NSArray *)_categories { + ASSIGN(categories, _categories); +} + +- (NSArray *)categories { + return categories; +} + +/* class */ + +#if 0 +- (NSArray *)accessClassItems { + static NSArray classItems = nil; + + if (!classItems) { + return [[NSArray arrayWithObjects:@"PUBLIC", @"PRIVATE", nil] retain]; + } + return classItems; +} +#endif + +- (void)setAccessClass:(NSString *)_class { + ASSIGN(accessClass, _class); +} +- (NSString *)accessClass { + return accessClass; +} + +- (void)setIsPrivate:(BOOL)_yn { + if (_yn) + [self setAccessClass:@"PRIVATE"]; + else + [self setAccessClass:@"PUBLIC"]; + isPrivate = _yn; +} +- (BOOL)isPrivate { + return isPrivate; +} + +- (void)setCheckForConflicts:(BOOL)_checkForConflicts { + checkForConflicts = _checkForConflicts; +} +- (BOOL)checkForConflicts { + return checkForConflicts; +} + +- (NSArray *)cycles { + static NSArray *cycles = nil; + + if (!cycles) { + NSBundle *bundle; + NSString *path; + + bundle = [NSBundle bundleForClass:[self class]]; + path = [bundle pathForResource:@"cycles" ofType:@"plist"]; + NSAssert(path != nil, @"Cannot find cycles.plist!"); + cycles = [[NSArray arrayWithContentsOfFile:path] retain]; + NSAssert(cycles != nil, @"Cannot instantiate cycles from cycles.plist!"); + } + return cycles; +} + +- (void)setCycle:(NSDictionary *)_cycle { + ASSIGN(cycle, _cycle); +} +- (NSDictionary *)cycle { + return cycle; +} +- (BOOL)hasCycle { + [self debugWithFormat:@"cycle: %@", cycle]; + if (![cycle objectForKey:@"rule"]) + return NO; + return YES; +} +- (NSString *)cycleLabel { + NSString *key; + + key = [(NSDictionary *)item objectForKey:@"label"]; + return [self labelForKey:key]; +} + + +- (void)setCycleUntilDate:(NSCalendarDate *)_cycleUntilDate { + NSCalendarDate *until; + + /* copy hour/minute/second from startDate */ + until = [_cycleUntilDate hour:[startDate hourOfDay] + minute:[startDate minuteOfHour] + second:[startDate secondOfMinute]]; + [until setTimeZone:[startDate timeZone]]; + ASSIGN(cycleUntilDate, until); +} +- (NSCalendarDate *)cycleUntilDate { + return cycleUntilDate; +} + +- (iCalRecurrenceRule *)rrule { + NSString *ruleRep; + iCalRecurrenceRule *rule; + + if (![self hasCycle]) + return nil; + ruleRep = [cycle objectForKey:@"rule"]; + rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:ruleRep]; + + if (cycleUntilDate && [self isCycleEndUntil]) + [rule setUntilDate:cycleUntilDate]; + return rule; +} + +- (void)adjustCycleControlsForRRule:(iCalRecurrenceRule *)_rrule { + NSDictionary *c; + NSCalendarDate *until; + + c = [self cycleMatchingRRule:_rrule]; + [self setCycle:c]; + + until = [[[_rrule untilDate] copy] autorelease]; + if (!until) + until = startDate; + else + [self setIsCycleEndUntil]; + + [until setTimeZone:[[self clientObject] userTimeZone]]; + [self setCycleUntilDate:until]; +} + +/* + This method is necessary, because we have a fixed sets of cycles in the UI. + The model is able to represent arbitrary rules, however. + There SHOULD be a different UI, similar to iCal.app, to allow modelling + of more complex rules. + + This method obviously cannot map all existing rules back to the fixed list + in cycles.plist. This should be fixed in a future version when interop + becomes more important. + */ +- (NSDictionary *)cycleMatchingRRule:(iCalRecurrenceRule *)_rrule { + NSString *cycleRep; + NSArray *cycles; + unsigned i, count; + + if (!_rrule) + return [[self cycles] objectAtIndex:0]; + + cycleRep = [_rrule versitString]; + cycles = [self cycles]; + count = [cycles count]; + for (i = 1; i < count; i++) { + NSDictionary *c; + NSString *cr; + + c = [cycles objectAtIndex:i]; + cr = [c objectForKey:@"rule"]; + if ([cr isEqualToString:cycleRep]) + return c; + } + [self warnWithFormat:@"No default cycle for rrule found! -> %@", _rrule]; + return nil; +} + +/* cycle "ends" - supposed to be 'never', 'COUNT' or 'UNTIL' */ +- (NSArray *)cycleEnds { + static NSArray *ends = nil; + + if (!ends) { + ends = [[NSArray alloc] initWithObjects:@"cycle_end_never", + @"cycle_end_until", + nil]; + } + return ends; +} + +- (void)setCycleEnd:(NSString *)_cycleEnd { + ASSIGNCOPY(cycleEnd, _cycleEnd); +} +- (NSString *)cycleEnd { + return cycleEnd; +} +- (BOOL)isCycleEndUntil { + return (cycleEnd && + [cycleEnd isEqualToString:@"cycle_end_until"]); +} +- (void)setIsCycleEndUntil { + [self setCycleEnd:@"cycle_end_until"]; +} +- (void)setIsCycleEndNever { + [self setCycleEnd:@"cycle_end_never"]; +} + +/* iCal */ + +- (void)setICalString:(NSString *)_s { + ASSIGNCOPY(iCalString, _s); +} +- (NSString *)iCalString { + return iCalString; +} + +- (NSString *)iCalStringTemplate { + static NSString *iCalStringTemplate = \ + @"BEGIN:VCALENDAR\r\n" + @"METHOD:REQUEST\r\n" + @"PRODID:OpenGroupware.org SOGo 0.9\r\n" + @"VERSION:2.0\r\n" + @"BEGIN:VTODO\r\n" + @"UID:%@\r\n" + @"CLASS:PUBLIC\r\n" + @"STATUS:NEEDS-ACTION\r\n" /* confirmed by default */ + @"PERCENT-COMPLETE:0\r\n" + @"DTSTAMP:%@Z\r\n" + @"DTSTART:%@\r\n" + @"DUE:%@\r\n" + @"SEQUENCE:1\r\n" + @"PRIORITY:5\r\n" + @"%@" /* organizer */ + @"%@" /* participants and resources */ + @"END:VTODO\r\n" + @"END:VCALENDAR"; + + NSCalendarDate *lStartDate, *lDueDate, *stamp; + NSString *template, *s; + unsigned minutes; + + s = [self queryParameterForKey:@"dur"]; + if(s && [s length] > 0) { + minutes = [s intValue]; + } + else { + minutes = 60; + } + lStartDate = [self selectedDate]; + lDueDate = [lStartDate dateByAddingYears:0 months:0 days:0 + hours:0 minutes:minutes seconds:0]; + + stamp = [NSCalendarDate calendarDate]; + [stamp setTimeZone: [NSTimeZone timeZoneWithName: @"GMT"]]; + + s = [self iCalParticipantsAndResourcesStringFromQueryParameters]; + template = [NSString stringWithFormat:iCalStringTemplate, + [[self clientObject] nameInContainer], + [stamp iCalFormattedDateTimeString], + [lStartDate iCalFormattedDateTimeString], + [lDueDate iCalFormattedDateTimeString], + [self iCalOrganizerString], + s]; + return template; +} + +- (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters { + NSString *s; + + s = [self iCalParticipantsStringFromQueryParameters]; + return [s stringByAppendingString: + [self iCalResourcesStringFromQueryParameters]]; +} + +- (NSString *)iCalParticipantsStringFromQueryParameters { + static NSString *iCalParticipantString = \ + @"ATTENDEE;ROLE=REQ-PARTICIPANT;CN=\"%@\":mailto:%@\r\n"; + + return [self iCalStringFromQueryParameter:@"ps" + format:iCalParticipantString]; +} + +- (NSString *)iCalResourcesStringFromQueryParameters { + static NSString *iCalResourceString = \ + @"ATTENDEE;ROLE=NON-PARTICIPANT;CN=\"%@\":mailto:%@\r\n"; + + return [self iCalStringFromQueryParameter:@"rs" + format:iCalResourceString]; +} + +- (NSString *)iCalStringFromQueryParameter:(NSString *)_qp + format:(NSString *)_format +{ + AgenorUserManager *um; + NSMutableString *iCalRep; + NSString *s; + + um = [AgenorUserManager sharedUserManager]; + iCalRep = (NSMutableString *)[NSMutableString string]; + s = [self queryParameterForKey:_qp]; + if(s && [s length] > 0) { + NSArray *es; + unsigned i, count; + + es = [s componentsSeparatedByString:@","]; + count = [es count]; + for(i = 0; i < count; i++) { + NSString *email, *cn; + + email = [es objectAtIndex:i]; + cn = [um getCNForUID:[um getUIDForEmail:email]]; + [iCalRep appendFormat:_format, cn, email]; + } + } + return iCalRep; +} + +- (NSString *)iCalOrganizerString { + static NSString *fmt = @"ORGANIZER;CN=\"%@\":mailto:%@\r\n"; + return [NSString stringWithFormat:fmt, + [self cnForUser], + [self emailForUser]]; +} + +#if 0 +- (iCalPerson *)getOrganizer { + iCalPerson *p; + NSString *emailProp; + + emailProp = [@"mailto:" stringByAppendingString:[self emailForUser]]; + p = [[[iCalPerson alloc] init] autorelease]; + [p setEmail:emailProp]; + [p setCn:[self cnForUser]]; + return p; +} +#endif + + +/* helper */ + +- (NSString *)_completeURIForMethod:(NSString *)_method { + NSString *uri; + NSRange r; + + uri = [[[self context] request] uri]; + + /* first: identify query parameters */ + r = [uri rangeOfString:@"?" options:NSBackwardsSearch]; + if (r.length > 0) + uri = [uri substringToIndex:r.location]; + + /* next: append trailing slash */ + if (![uri hasSuffix:@"/"]) + uri = [uri stringByAppendingString:@"/"]; + + /* next: append method */ + uri = [uri stringByAppendingString:_method]; + + /* next: append query parameters */ + return [self completeHrefForMethod:uri]; +} + +/* new */ + +- (id)newAction { + /* + This method creates a unique ID and redirects to the "edit" method on the + new ID. + It is actually a folder method and should be defined on the folder. + + Note: 'clientObject' is the SOGoAppointmentFolder! + Update: remember that there are group folders as well. + */ + NSString *uri, *objectId, *method, *ps; + + objectId = [NSClassFromString(@"SOGoAppointmentFolder") + globallyUniqueObjectId]; + if ([objectId length] == 0) { + return [NSException exceptionWithHTTPStatus:500 /* Internal Error */ + reason:@"could not create a unique ID"]; + } + + method = [NSString stringWithFormat:@"Calendar/%@/editAsTask", objectId]; + method = [[self userFolderPath] stringByAppendingPathComponent:method]; + + /* check if participants have already been provided */ + ps = [self queryParameterForKey:@"ps"]; +// if (ps) { +// [self setQueryParameter:ps forKey:@"ps"]; +// } + if (!ps + && [[self clientObject] respondsToSelector:@selector(calendarUIDs)]) { + AgenorUserManager *um; + NSArray *uids; + NSMutableArray *emails; + unsigned i, count; + + /* add all current calendarUIDs as default participants */ + + um = [AgenorUserManager sharedUserManager]; + uids = [[self clientObject] calendarUIDs]; + count = [uids count]; + emails = [NSMutableArray arrayWithCapacity:count]; + + for (i = 0; i < count; i++) { + NSString *email; + + email = [um getEmailForUID:[uids objectAtIndex:i]]; + if (email) + [emails addObject:email]; + } + ps = [emails componentsJoinedByString:@","]; + [self setQueryParameter:ps forKey:@"ps"]; + } + uri = [self completeHrefForMethod:method]; + return [self redirectToLocation:uri]; +} + +/* save */ + +/* returned dates are in GMT */ +- (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values + treatAsResource:(BOOL)_isResource +{ + unsigned i, count; + NSMutableArray *result; + + count = [_values count]; + result = [[NSMutableArray alloc] initWithCapacity:count]; + for (i = 0; i < count; i++) { + NSString *pString, *email, *cn; + NSRange r; + iCalPerson *p; + + pString = [_values objectAtIndex:i]; + if ([pString length] == 0) + continue; + + /* delimiter between email and cn */ + r = [pString rangeOfString:@";"]; + if (r.length > 0) { + email = [pString substringToIndex:r.location]; + cn = (r.location + 1 < [pString length]) + ? [pString substringFromIndex:r.location + 1] + : nil; + } + else { + email = pString; + cn = nil; + } + if (cn == nil) { + /* fallback */ + AgenorUserManager *um = [AgenorUserManager sharedUserManager]; + cn = [um getCNForUID:[um getUIDForEmail:email]]; + } + + p = [[iCalPerson alloc] init]; + [p setEmail:[@"mailto:" stringByAppendingString:email]]; + if ([cn isNotNull]) [p setCn:cn]; + + /* see RFC2445, sect. 4.2.16 for details */ + [p setRole:_isResource ? @"NON-PARTICIPANT" : @"REQ-PARTICIPANT"]; + [result addObject:p]; + [p release]; + } + return [result autorelease]; +} + +- (BOOL)isWriteableClientObject { + return [[self clientObject] + respondsToSelector:@selector(saveContentString:)]; +} + +- (NSException *)validateObjectForStatusChange { + BOOL ok; + id co; + + co = [self clientObject]; + ok = [co respondsToSelector:@selector(changeParticipationStatus:inContext:)]; + if (!ok) { + return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ + reason: + @"method cannot be invoked on the specified object"]; + } + return nil; +} + +- (void)loadValuesFromTask: (iCalToDo *)_task +{ + NSString *s; + iCalRecurrenceRule *rrule; + NSTimeZone *uTZ; + + if ((startDate = [_task startDate]) == nil) + startDate = [[NSCalendarDate date] hour:11 minute:0]; + if ((dueDate = [_task due]) == nil) { + dueDate = + [startDate hour:[startDate hourOfDay] + 1 minute:0]; + } + + uTZ = [[self clientObject] userTimeZone]; + [startDate setTimeZone: uTZ]; + [dueDate setTimeZone: uTZ]; +// startDate = [startDate adjustedDate]; +// dueDate = [dueDate adjustedDate]; + [startDate retain]; + [dueDate retain]; + + title = [[_task summary] copy]; + location = [[_task location] copy]; + comment = [[_task comment] copy]; + priority = [[_task priority] copy]; + categories = [[[_task categories] commaSeparatedValues] retain]; + organizer = [[_task organizer] retain]; + participants = [[_task participants] retain]; + resources = [[_task resources] retain]; + +// NSLog (@"summary éàè: '%@'", title); + + s = [_task accessClass]; + if(!s || [s isEqualToString:@"PUBLIC"]) + [self setIsPrivate:NO]; + else + [self setIsPrivate:YES]; /* we're possibly loosing information here */ + + /* cycles */ + if ([_task isRecurrent]) + { + rrule = [[_task recurrenceRules] objectAtIndex: 0]; + [self adjustCycleControlsForRRule:rrule]; + } +} + +- (void)saveValuesIntoTask:(iCalToDo *)_task { + /* merge in form values */ + NSArray *attendees, *lResources; + iCalRecurrenceRule *rrule; + + [_task setStartDate: [self taskStartDate]]; + [_task setDue: [self taskDueDate]]; + + [_task setSummary: [self title]]; + [_task setLocation: [self location]]; + [_task setComment: [self comment]]; + [_task setPriority:[self priority]]; + [_task setCategories: [[self categories] componentsJoinedByString: @","]]; + + [_task setAccessClass:[self accessClass]]; + +#if 0 + /* + Note: bad, bad, bad! + Organizer is no form value, thus we MUST NOT change it + */ + [_task setOrganizer:organizer]; +#endif + attendees = [self participants]; + lResources = [self resources]; + if ([lResources count] > 0) { + attendees = ([attendees count] > 0) + ? [attendees arrayByAddingObjectsFromArray:lResources] + : lResources; + } + [attendees makeObjectsPerformSelector: @selector (setTag:) + withObject: @"attendee"]; + [_task setAttendees:attendees]; + + /* cycles */ + [_task removeAllRecurrenceRules]; + rrule = [self rrule]; + if (rrule) + [_task addToRecurrenceRules: rrule]; +} + +- (iCalToDo *) taskFromString: (NSString *) _iCalString +{ + iCalCalendar *calendar; + iCalToDo *task; + SOGoTaskObject *clientObject; + + clientObject = [self clientObject]; + calendar = [iCalCalendar parseSingleFromSource: _iCalString]; + task = [clientObject firstTaskFromCalendar: calendar]; + + return task; +} + +/* contact editor compatibility */ + +- (void)setContentString:(NSString *)_s { + [self setICalString:_s]; +} +- (NSString *)contentStringTemplate { + return [self iCalStringTemplate]; +} + +/* access */ + +- (BOOL) isMyTask +{ + // TODO: this should check a set of emails against the SoUser + return (![[organizer email] length] + || [[organizer rfc822Email] isEqualToString: [self emailForUser]]); +} + +- (BOOL)canAccessTask { + return [self isMyTask]; +} + +- (BOOL)canEditTask { + return [self isMyTask]; +} + + +/* conflict management */ + +- (BOOL)containsConflict:(iCalToDo *)_task { + NSArray *attendees, *uids; + SOGoAppointmentFolder *groupCalendar; + NSArray *infos; + NSArray *ranges; + id folder; + + [self logWithFormat:@"search from %@ to %@", + [_task startDate], [_task due]]; + + folder = [[self clientObject] container]; + attendees = [_task attendees]; + uids = [folder uidsFromICalPersons:attendees]; + if ([uids count] == 0) { + [self logWithFormat:@"Note: no UIDs selected."]; + return NO; + } + + groupCalendar = [folder lookupGroupCalendarFolderForUIDs:uids + inContext:[self context]]; + [self debugWithFormat:@"group calendar: %@", groupCalendar]; + + if (![groupCalendar respondsToSelector:@selector(fetchFreebusyInfosFrom:to:)]) { + [self errorWithFormat:@"invalid folder to run freebusy query on!"]; + return NO; + } + + infos = [groupCalendar fetchFreebusyInfosFrom:[_task startDate] + to:[_task due]]; + [self debugWithFormat:@" process: %d tasks", [infos count]]; + + ranges = [infos arrayByCreatingDateRangesFromObjectsWithStartDateKey:@"startDate" + andDueDateKey:@"dueDate"]; + ranges = [ranges arrayByCompactingContainedDateRanges]; + [self debugWithFormat:@" blocked ranges: %@", ranges]; + + return [ranges count] != 0 ? YES : NO; +} + +/* response generation */ + +- (NSString *)initialCycleVisibility { + if (![self hasCycle]) + return @"visibility: hidden;"; + return @"visibility: visible;"; +} + +- (NSString *)initialCycleEndUntilVisibility { + if ([self isCycleEndUntil]) + return @"visibility: visible;"; + return @"visibility: hidden;"; +} + + +/* actions */ + +- (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{ + return YES; +} + +- (id)testAction { + /* for testing only */ + WORequest *req; + iCalToDo *task; + NSString *content; + + req = [[self context] request]; + task = [self taskFromString: [self iCalString]]; + [self saveValuesIntoTask:task]; + content = [[task parent] versitString]; + [self logWithFormat:@"%s -- iCal:\n%@", + __PRETTY_FUNCTION__, + content]; + + return self; +} + +- (id)defaultAction { + NSString *ical; + + /* load iCalendar file */ + + // TODO: can't we use [clientObject contentAsString]? +// ical = [[self clientObject] valueForKey:@"iCalString"]; + ical = [[self clientObject] contentAsString]; + if ([ical length] == 0) /* a new task */ + ical = [self contentStringTemplate]; + + [self setContentString:ical]; + [self loadValuesFromTask: [self taskFromString: ical]]; + + if (![self canEditTask]) { + /* TODO: we need proper ACLs */ + return [self redirectToLocation:[self _completeURIForMethod:@"../view"]]; + } + return self; +} + +- (id)saveAction { + iCalToDo *task; + iCalPerson *p; + NSString *content; + NSException *ex; + + if (![self isWriteableClientObject]) { + /* return 400 == Bad Request */ + return [NSException exceptionWithHTTPStatus:400 + reason:@"method cannot be invoked on " + @"the specified object"]; + } + + task = [self taskFromString: [self iCalString]]; + if (task == nil) { + NSString *s; + + s = [self labelForKey:@"Invalid iCal data!"]; + [self setErrorText:s]; + return self; + } + + [self saveValuesIntoTask:task]; + p = [task findParticipantWithEmail:[self emailForUser]]; + if (p) { + [p setParticipationStatus:iCalPersonPartStatAccepted]; + } + + if ([self checkForConflicts]) { + if ([self containsConflict:task]) { + NSString *s; + + s = [self labelForKey:@"Conflicts found!"]; + [self setErrorText:s]; + + return self; + } + } + content = [[task parent] versitString]; +// [task release]; task = nil; + + if (content == nil) { + NSString *s; + + s = [self labelForKey:@"Could not create iCal data!"]; + [self setErrorText:s]; + return self; + } + + ex = [[self clientObject] saveContentString:content]; + if (ex != nil) { + [self setErrorText:[ex reason]]; + return self; + } + + return [self redirectToLocation:[self _completeURIForMethod:@".."]]; +} + +- (id) changeStatusAction +{ + iCalToDo *task; + SOGoTaskObject *taskObject; + NSString *content; + id ex; + int newStatus; + + newStatus = [[self queryParameterForKey: @"status"] intValue]; + + taskObject = [self clientObject]; + task = [taskObject task]; + switch (newStatus) + { + case 1: + [task setCompleted: [NSCalendarDate calendarDate]]; + break; + case 2: + [task setStatus: @"IN-PROCESS"]; + break; + case 3: + [task setStatus: @"CANCELLED"]; + break; + default: + [task setStatus: @"NEEDS-ACTION"]; + } + + content = [[task parent] versitString]; + ex = [[self clientObject] saveContentString: content]; + if (ex != nil) { + [self setErrorText:[ex reason]]; + return self; + } + + return [self redirectToLocation: [self _completeURIForMethod: @".."]]; +} + +- (id)acceptAction { + return [self acceptOrDeclineAction:YES]; +} + +- (id)declineAction { + return [self acceptOrDeclineAction:NO]; +} + +- (NSString *) saveUrl +{ + return [NSString stringWithFormat: @"%@/saveAsTask", + [[self clientObject] baseURL]]; +} + +// TODO: add tentatively + +- (id)acceptOrDeclineAction:(BOOL)_accept { + // TODO: this should live in the SoObjects + NSException *ex; + + if ((ex = [self validateObjectForStatusChange]) != nil) + return ex; + + ex = [[self clientObject] changeParticipationStatus: + _accept ? @"ACCEPTED" : @"DECLINED" + inContext:[self context]]; + if (ex != nil) return ex; + + return [self redirectToLocation:[self _completeURIForMethod:@"../view"]]; +} + +@end /* UIxTaskEditor */ diff --git a/UI/Scheduler/UIxTaskProposal.m b/UI/Scheduler/UIxTaskProposal.m new file mode 100644 index 000000000..f2b35e821 --- /dev/null +++ b/UI/Scheduler/UIxTaskProposal.m @@ -0,0 +1,488 @@ +/* + Copyright (C) 2004 SKYRIX Software AG + + This file is part of OpenGroupware.org. + + OGo 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 + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with OGo; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ +// $Id: UIxTaskEditor.m 181 2004-08-11 15:13:25Z helge $ + +#include + +@interface UIxTaskProposal : UIxComponent +{ + id item; + id currentDay; + + /* individual values */ + id startDateHour; + id startDateMinute; + id endDateHour; + id endDateMinute; + + id startDateDay; + id startDateMonth; + id startDateYear; + id endDateDay; + id endDateMonth; + id endDateYear; + + NSArray *participants; /* array of iCalPerson's */ + NSArray *resources; /* array of iCalPerson's */ + id duration; + + NSArray *blockedRanges; + NSMutableDictionary *currentQueryParameters; +} + +- (NSMutableDictionary *)currentQueryParameters; +- (NSDictionary *)currentHourQueryParametersForFirstHalf:(BOOL)_first; + +- (void)setICalPersons:(NSArray *)_ps asQueryParameter:(NSString *)_qp; +@end + +#include +#include +#include +#include +#include "common.h" + +@implementation UIxTaskProposal + +- (void)dealloc { + [self->blockedRanges release]; + + [self->startDateHour release]; + [self->startDateMinute release]; + [self->startDateDay release]; + [self->startDateMonth release]; + [self->startDateYear release]; + + [self->endDateHour release]; + [self->endDateMinute release]; + [self->endDateDay release]; + [self->endDateMonth release]; + [self->endDateYear release]; + [self->duration release]; + + [self->participants release]; + [self->resources release]; + + [self->currentQueryParameters release]; + [super dealloc]; +} + +/* notifications */ + +- (void)sleep { + [self->currentDay release]; self->currentDay = nil; + [self->item release]; self->item = nil; + [super sleep]; +} + +/* accessors */ + +- (void)setItem:(id)_item { + ASSIGN(self->item, _item); +} +- (id)item { + return self->item; +} + +- (void)setStartDateHour:(id)_startDateHour { + ASSIGN(self->startDateHour, _startDateHour); +} +- (id)startDateHour { + return self->startDateHour; +} +- (void)setStartDateMinute:(id)_startDateMinute { + ASSIGN(self->startDateMinute, _startDateMinute); +} +- (id)startDateMinute { + return self->startDateMinute; +} +- (void)setStartDateDay:(id)_startDateDay { + ASSIGN(self->startDateDay, _startDateDay); +} +- (id)startDateDay { + return self->startDateDay; +} +- (void)setStartDateMonth:(id)_startDateMonth { + ASSIGN(self->startDateMonth, _startDateMonth); +} +- (id)startDateMonth { + return self->startDateMonth; +} +- (void)setStartDateYear:(id)_startDateYear { + ASSIGN(self->startDateYear, _startDateYear); +} +- (id)startDateYear { + return self->startDateYear; +} +- (void)setEndDateHour:(id)_endDateHour { + ASSIGN(self->endDateHour, _endDateHour); +} +- (id)endDateHour { + return self->endDateHour; +} +- (void)setEndDateMinute:(id)_endDateMinute { + ASSIGN(self->endDateMinute, _endDateMinute); +} +- (id)endDateMinute { + return self->endDateMinute; +} +- (void)setEndDateDay:(id)_endDateDay { + ASSIGN(self->endDateDay, _endDateDay); +} +- (id)endDateDay { + return self->endDateDay; +} +- (void)setEndDateMonth:(id)_endDateMonth { + ASSIGN(self->endDateMonth, _endDateMonth); +} +- (id)endDateMonth { + return self->endDateMonth; +} +- (void)setEndDateYear:(id)_endDateYear { + ASSIGN(self->endDateYear, _endDateYear); +} +- (id)endDateYear { + return self->endDateYear; +} + +- (void)setStartDate:(NSCalendarDate *)_date { + [self setStartDateHour:[NSNumber numberWithInt:[_date hourOfDay]]]; + [self setStartDateMinute:[NSNumber numberWithInt:[_date minuteOfHour]]]; + [self setStartDateDay:[NSNumber numberWithInt:[_date dayOfMonth]]]; + [self setStartDateMonth:[NSNumber numberWithInt:[_date monthOfYear]]]; + [self setStartDateYear:[NSNumber numberWithInt:[_date yearOfCommonEra]]]; +} +- (NSCalendarDate *)startDate { + return [NSCalendarDate dateWithYear:[[self startDateYear] intValue] + month:[[self startDateMonth] intValue] + day:[[self startDateDay] intValue] + hour:[[self startDateHour] intValue] + minute:[[self startDateMinute] intValue] + second:0 + timeZone:[[self clientObject] userTimeZone]]; +} +- (void)setEndDate:(NSCalendarDate *)_date { + [self setEndDateHour:[NSNumber numberWithInt:[_date hourOfDay]]]; + [self setEndDateMinute:[NSNumber numberWithInt:[_date minuteOfHour]]]; + [self setEndDateDay:[NSNumber numberWithInt:[_date dayOfMonth]]]; + [self setEndDateMonth:[NSNumber numberWithInt:[_date monthOfYear]]]; + [self setEndDateYear:[NSNumber numberWithInt:[_date yearOfCommonEra]]]; +} +- (NSCalendarDate *)endDate { + return [NSCalendarDate dateWithYear:[[self endDateYear] intValue] + month:[[self endDateMonth] intValue] + day:[[self endDateDay] intValue] + hour:[[self endDateHour] intValue] + minute:[[self endDateMinute] intValue] + second:59 + timeZone:[[self clientObject] userTimeZone]]; +} + +- (void)setDuration:(id)_duration { + ASSIGN(self->duration, _duration); +} +- (id)duration { + return self->duration; +} +- (int)durationInMinutes { + return [[self duration] intValue]; +} +- (NSTimeInterval)durationAsTimeInterval { + return [self durationInMinutes] * 60; +} + +- (NSString *)itemDurationText { + // TODO: use a formatter + // TODO: localize + switch ([[self item] intValue]) { + case 30: return @"30 minutes"; + case 60: return @"1 hour"; + case 120: return @"2 hours"; + case 240: return @"4 hours"; + case 480: return @"8 hours"; + default: + return [NSString stringWithFormat:@"%@ minutes", [self item]]; + } +} + +- (void)setParticipants:(NSArray *)_parts { + ASSIGN(self->participants, _parts); +} +- (NSArray *)participants { + return self->participants; +} +- (void)setResources:(NSArray *)_res { + ASSIGN(self->resources, _res); +} +- (NSArray *)resources { + return self->resources; +} + +- (NSArray *)attendees { + NSArray *a, *b; + + a = [self participants]; + b = [self resources]; + if ([b count] == 0) return a; + if ([a count] == 0) return b; + return [a arrayByAddingObjectsFromArray:b]; +} + +- (void)setCurrentDay:(id)_day { + ASSIGN(self->currentDay, _day); +} +- (id)currentDay { + return self->currentDay; +} + +- (NSArray *)hours { + // TODO: from 'earliest start' to 'latest endtime' + unsigned lStartHour = 9, lEndHour = 17, i; + NSMutableArray *ma; + + lStartHour = [[self startDateHour] intValue]; + lEndHour = [[self endDateHour] intValue]; + if (lStartHour < 1) lStartHour = 1; + if (lEndHour < lStartHour) lEndHour = lStartHour + 1; + + ma = [NSMutableArray arrayWithCapacity:lEndHour - lStartHour + 2]; + for (i = lStartHour; i <= lEndHour; i++) + [ma addObject:[NSNumber numberWithInt:i]]; + return ma; +} + +- (NSArray *)days { + // TODO: from startdate to enddate + NSMutableArray *ma; + NSCalendarDate *base, *stop, *current; + + base = [NSCalendarDate dateWithYear:[[self startDateYear] intValue] + month:[[self startDateMonth] intValue] + day:[[self startDateDay] intValue] + hour:12 minute:0 second:0 + timeZone:[[self clientObject] userTimeZone]]; + stop = [NSCalendarDate dateWithYear:[[self endDateYear] intValue] + month:[[self endDateMonth] intValue] + day:[[self endDateDay] intValue] + hour:12 minute:0 second:0 + timeZone:[[self clientObject] userTimeZone]]; + + ma = [NSMutableArray arrayWithCapacity:16]; + + current = base; + while ([current compare:stop] != NSOrderedDescending) { + [current setTimeZone:[[self clientObject] userTimeZone]]; + [ma addObject:current]; + + /* Note: remember the timezone behaviour of the method below! */ + current = [current dateByAddingYears:0 months:0 days:1]; + } + return ma; +} + +- (NSArray *)durationSteps { + // TODO: make configurable + return [NSArray arrayWithObjects: + @"30", @"60", @"120", @"240", @"480", nil]; +} + +/* slots */ + +- (BOOL)isRangeGreen:(NGCalendarDateRange *)_range { + unsigned idx; + + idx = [self->blockedRanges indexOfFirstIntersectingDateRange:_range]; + if (idx != NSNotFound) { + [self debugWithFormat:@"blocked range:\n range: %@\n block: %@\nintersection:%@", + _range, + [self->blockedRanges objectAtIndex:idx], + [_range intersectionDateRange:[self->blockedRanges objectAtIndex:idx]]]; + } + + return idx == NSNotFound ? YES : NO; +} + +- (BOOL)isSlotRangeGreen:(NGCalendarDateRange *)_slotRange { + NGCalendarDateRange *aptRange; + NSCalendarDate *aptStartDate, *aptEndDate; + + if (_slotRange == nil) + return NO; + + /* calculate the interval requested by the user (can be larger) */ + + aptStartDate = [_slotRange startDate]; + // TODO: gives warning on MacOSX + aptEndDate = [[NSCalendarDate alloc] initWithTimeIntervalSince1970: + [aptStartDate timeIntervalSince1970] + + [self durationAsTimeInterval]]; + [aptStartDate setTimeZone:[[self clientObject] userTimeZone]]; + [aptEndDate setTimeZone:[[self clientObject] userTimeZone]]; + aptRange = [NGCalendarDateRange calendarDateRangeWithStartDate:aptStartDate + endDate:aptEndDate]; + [aptEndDate release]; aptEndDate = nil; + + return [self isRangeGreen:aptRange]; +} + +- (BOOL)isFirstHalfGreen { + /* currentday is the date, self->item the hour */ + NSCalendarDate *from, *to; + NGCalendarDateRange *range; + + from = [self->currentDay hour:[[self item] intValue] minute:0]; + to = [self->currentDay hour:[[self item] intValue] minute:30]; + range = [NGCalendarDateRange calendarDateRangeWithStartDate:from endDate:to]; + return [self isSlotRangeGreen:range]; +} +- (BOOL)isSecondHalfGreen { + /* currentday is the date, self->item the hour */ + NSCalendarDate *from, *to; + NGCalendarDateRange *range; + + from = [self->currentDay hour:[[self item] intValue] minute:30]; + to = [self->currentDay hour:[[self item] intValue] + 1 minute:0]; + range = [NGCalendarDateRange calendarDateRangeWithStartDate:from endDate:to]; + return [self isSlotRangeGreen:range]; +} + +- (BOOL)isFirstHalfBlocked { + return [self isFirstHalfGreen] ? NO : YES; +} +- (BOOL)isSecondHalfBlocked { + return [self isSecondHalfGreen] ? NO : YES; +} + +/* actions */ + +- (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{ + return YES; +} + +- (id)defaultAction { + NSCalendarDate *now; + + now = [NSCalendarDate date]; + + [self setDuration:@"120"]; /* 1 hour as default */ + [self setStartDate:[now hour:9 minute:0]]; + [self setEndDate:[now hour:18 minute:0]]; + + return self; +} + +- (id)proposalSearchAction { + NSArray *attendees, *uids, *fbos, *ranges; + unsigned i, count; + NSMutableArray *allInfos; + + [self logWithFormat:@"search from %@ to %@", + [self startDate], [self endDate]]; + + attendees = [self attendees]; + uids = [[self clientObject] uidsFromICalPersons:attendees]; + if ([uids count] == 0) { + [self logWithFormat:@"Note: no UIDs selected."]; + return self; + } + + fbos = [[self clientObject] lookupFreeBusyObjectsForUIDs:uids + inContext:[self context]]; + count = [fbos count]; + // wild guess at capacity + allInfos = [[[NSMutableArray alloc] initWithCapacity:count * 10] autorelease]; + + for (i = 0; i < count; i++) { + SOGoFreeBusyObject *fb; + NSArray *infos; + + fb = [fbos objectAtIndex:i]; + if (fb != (SOGoFreeBusyObject *)[NSNull null]) { + infos = [fb fetchFreebusyInfosFrom:[self startDate] to:[self endDate]]; + [allInfos addObjectsFromArray:infos]; + } + } + [self debugWithFormat:@" processing: %d infos", [allInfos count]]; + ranges = [allInfos arrayByCreatingDateRangesFromObjectsWithStartDateKey: + @"startDate" + andEndDateKey:@"endDate"]; + ranges = [ranges arrayByCompactingContainedDateRanges]; + [self debugWithFormat:@" ranges: %@", ranges]; + + ASSIGNCOPY(self->blockedRanges, ranges); + + return self; +} + +/* URLs */ + +- (NSMutableDictionary *)currentQueryParameters { + if(!self->currentQueryParameters) { + self->currentQueryParameters = [[self queryParameters] mutableCopy]; + [self->currentQueryParameters setObject:[self duration] forKey:@"dur"]; + [self setICalPersons:[self resources] asQueryParameter:@"rs"]; + [self setICalPersons:[self participants] asQueryParameter:@"ps"]; + } + return self->currentQueryParameters; +} + +- (void)setICalPersons:(NSArray *)_ps asQueryParameter:(NSString *)_qp { + NSMutableString *s; + unsigned i, count; + + s = [[NSMutableString alloc] init]; + count = [_ps count]; + for(i = 0; i < count; i++) { + iCalPerson *p = [_ps objectAtIndex:i]; + [s appendString:[p rfc822Email]]; + if(i != (count - 1)) + [s appendString:@","]; + } + [[self currentQueryParameters] setObject:s forKey:_qp]; + [s release]; +} + +- (NSDictionary *)currentHourQueryParametersForFirstHalf:(BOOL)_first { + NSMutableDictionary *qp; + NSString *hmString; + NSCalendarDate *date; + unsigned minute; + + minute = _first ? 0 : 30; + qp = [self currentQueryParameters]; + + date = [self currentDay]; + hmString = [NSString stringWithFormat:@"%02d%02d", + [item intValue], minute]; + [self setSelectedDateQueryParameter:date inDictionary:qp]; + [qp setObject:hmString forKey:@"hm"]; + return qp; +} + +- (NSDictionary *)currentFirstHalfQueryParameters { + return [self currentHourQueryParametersForFirstHalf:YES]; +} + +- (NSDictionary *)currentSecondHalfQueryParameters { + return [self currentHourQueryParametersForFirstHalf:NO]; +} + +@end /* UIxTaskProposal */ diff --git a/UI/Scheduler/UIxTaskView.h b/UI/Scheduler/UIxTaskView.h new file mode 100644 index 000000000..51678d718 --- /dev/null +++ b/UI/Scheduler/UIxTaskView.h @@ -0,0 +1,40 @@ +// $Id: UIxTaskView.h 768 2005-07-15 00:13:01Z helge $ + +#ifndef __SOGo_UIxTaskView_H__ +#define __SOGo_UIxTaskView_H__ + +#include + +@class NSCalendarDate; +@class iCalToDo; +@class iCalPerson; +@class SOGoDateFormatter; + +@interface UIxTaskView : UIxComponent +{ + iCalToDo* task; + iCalPerson* attendee; + SOGoDateFormatter *dateFormatter; + id item; +} + +- (iCalToDo *) task; + +/* permissions */ +- (BOOL)canAccessApt; +- (BOOL)canEditApt; + +- (SOGoDateFormatter *)dateFormatter; +- (NSCalendarDate *)startTime; +- (NSCalendarDate *)endTime; + +- (NSString *)attributesTabLink; +- (NSString *)participantsTabLink; + +- (NSString *)completeHrefForMethod:(NSString *)_method + withParameter:(NSString *)_param + forKey:(NSString *)_key; + +@end + +#endif /* __SOGo_UIxTaskView_H__ */ diff --git a/UI/Scheduler/UIxTaskView.m b/UI/Scheduler/UIxTaskView.m new file mode 100644 index 000000000..71bf79c82 --- /dev/null +++ b/UI/Scheduler/UIxTaskView.m @@ -0,0 +1,296 @@ +/* + Copyright (C) 2004-2005 SKYRIX Software AG + + This file is part of OpenGroupware.org. + + OGo 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 + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with OGo; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ + +#import "UIxTaskView.h" +#import +#import +#import +#import +#import "UIxComponent+Agenor.h" +#import "common.h" + +@interface UIxTaskView (PrivateAPI) +- (BOOL)isAttendeeActiveUser; +@end + +@implementation UIxTaskView + +- (void)dealloc { + [task release]; + [attendee release]; + [dateFormatter release]; + [item release]; + [super dealloc]; +} + +/* accessors */ + +- (NSString *)tabSelection { + NSString *selection; + + selection = [self queryParameterForKey:@"tab"]; + if (selection == nil) + selection = @"attributes"; + return selection; +} + +- (void)setAttendee:(id)_attendee { + ASSIGN(attendee, _attendee); +} +- (id)attendee { + return attendee; +} + +- (BOOL)isAttendeeActiveUser { + NSString *email, *attEmail; + + email = [[[self context] activeUser] email]; + attendee = [self attendee]; + attEmail = [attendee rfc822Email]; + + return [email isEqualToString: attEmail]; +} + +- (BOOL)showAcceptButton { + return [[self attendee] participationStatus] != iCalPersonPartStatAccepted; +} +- (BOOL)showRejectButton { + return [[self attendee] participationStatus] != iCalPersonPartStatDeclined; +} +- (NSString *)attendeeStatusColspan { + return [self isAttendeeActiveUser] ? @"1" : @"2"; +} + +- (void)setItem:(id)_item { + ASSIGN(item, _item); +} +- (id)item { + return item; +} + +- (SOGoDateFormatter *)dateFormatter { + if (dateFormatter == nil) { + dateFormatter = + [[SOGoDateFormatter alloc] initWithLocale:[self locale]]; + [dateFormatter setFullWeekdayNameAndDetails]; + } + return dateFormatter; +} + +- (NSCalendarDate *)startTime { + NSCalendarDate *date; + + date = [[self task] startDate]; + [date setTimeZone:[[self clientObject] userTimeZone]]; + return date; +} + +- (NSCalendarDate *)endTime { + NSCalendarDate *date; + + date = [[self task] due]; + [date setTimeZone:[[self clientObject] userTimeZone]]; + return date; +} + +- (NSString *)resourcesAsString { + NSArray *resources, *cns; + + resources = [[self task] resources]; + cns = [resources valueForKey:@"cnForDisplay"]; + return [cns componentsJoinedByString:@"
"]; +} + +- (NSString *) categoriesAsString +{ + NSEnumerator *categories; + NSArray *rawCategories; + NSMutableArray *l10nCategories; + NSString *currentCategory, *l10nCategory; + + rawCategories + = [[task categories] componentsSeparatedByString: @","]; + l10nCategories = [NSMutableArray arrayWithCapacity: [rawCategories count]]; + categories = [rawCategories objectEnumerator]; + currentCategory = [categories nextObject]; + while (currentCategory) + { + l10nCategory + = [self labelForKey: [currentCategory stringByTrimmingSpaces]]; + if (l10nCategory) + [l10nCategories addObject: l10nCategory]; + currentCategory = [categories nextObject]; + } + + return [l10nCategories componentsJoinedByString: @", "]; +} + +// task.organizer.cnForDisplay +- (NSString *) eventOrganizer +{ + CardElement *organizer; + + organizer = [[self task] uniqueChildWithTag: @"organizer"]; + + return [organizer value: 0 ofAttribute: @"cn"]; +} + +- (NSString *) priorityLabelKey +{ + return [NSString stringWithFormat: @"prio_%@", [task priority]]; +} + +/* backend */ + +- (iCalToDo *) task +{ + NSString *iCalString; + iCalCalendar *calendar; + SOGoTaskObject *clientObject; + + if (task != nil) + return task; + + clientObject = [self clientObject]; + + iCalString = [[self clientObject] valueForKey:@"iCalString"]; + if (![iCalString isNotNull] || [iCalString length] == 0) { + [self errorWithFormat:@"(%s): missing iCal string!", + __PRETTY_FUNCTION__]; + return nil; + } + + calendar = [iCalCalendar parseSingleFromSource: iCalString]; + task = [clientObject firstTaskFromCalendar: calendar]; + [task retain]; + + return task; +} + +/* hrefs */ + +- (NSString *)attributesTabLink { + return [self completeHrefForMethod:[self ownMethodName] + withParameter:@"attributes" + forKey:@"tab"]; +} + +- (NSString *)participantsTabLink { + return [self completeHrefForMethod:[self ownMethodName] + withParameter:@"participants" + forKey:@"tab"]; +} + +- (NSString *)debugTabLink { + return [self completeHrefForMethod:[self ownMethodName] + withParameter:@"debug" + forKey:@"tab"]; +} + +- (NSString *)completeHrefForMethod:(NSString *)_method + withParameter:(NSString *)_param + forKey:(NSString *)_key +{ + NSString *href; + + [self setQueryParameter:_param forKey:_key]; + href = [self completeHrefForMethod:[self ownMethodName]]; + [self setQueryParameter:nil forKey:_key]; + return href; +} + + +/* access */ + +- (BOOL)isMyApt { + NSString *email; + iCalPerson *organizer; + + email = [[[self context] activeUser] email]; + organizer = [[self task] organizer]; + if (!organizer) return YES; // assume this is correct to do, right? + return [[organizer rfc822Email] isEqualToString:email]; +} + +- (BOOL)canAccessApt { + NSString *email; + NSArray *partMails; + + if ([self isMyApt]) + return YES; + + /* not my apt - can access if it's public */ + if ([[[self task] accessClass] isEqualToString: @"PUBLIC"]) + return YES; + + /* can access it if I'm invited :-) */ + email = [[[self context] activeUser] email]; + partMails = [[[self task] participants] valueForKey:@"rfc822Email"]; + return [partMails containsObject:email]; +} + +- (BOOL)canEditApt { + return [self isMyApt]; +} + + +/* action */ + +- (id)defaultAction { + if ([self task] == nil) { + return [NSException exceptionWithHTTPStatus:404 /* Not Found */ + reason:@"could not locate task"]; + } + + return self; +} + +- (BOOL)isDeletableClientObject { + return [[self clientObject] respondsToSelector:@selector(delete)]; +} + +- (id)deleteAction { + NSException *ex; + id url; + + if ([self task] == nil) { + return [NSException exceptionWithHTTPStatus:404 /* Not Found */ + reason:@"could not locate task"]; + } + + if (![self isDeletableClientObject]) { + /* return 400 == Bad Request */ + return [NSException exceptionWithHTTPStatus:400 + reason:@"method cannot be invoked on " + @"the specified object"]; + } + + if ((ex = [[self clientObject] delete]) != nil) { + // TODO: improve error handling + [self debugWithFormat:@"failed to delete: %@", ex]; + return ex; + } + + url = [[[self clientObject] container] baseURLInContext:[self context]]; + return [self redirectToLocation:url]; +} + +@end /* UIxTaskView */