Merge commit 'a4cb4d96face514924387d34746b3148848ac092' into zope-2.13
[Plinn.git] / CalendarTool.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # Copyright (C) 2005-2007 BenoƮt PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Plinn calendar tool provides utilities to display content on a calendar layout
21
22
23
24 """
25
26 from Products.CMFCore.utils import UniqueObject, getToolByName
27 from Products.CMFCore.permissions import ManagePortal, ListFolderContents
28 from Products.CMFCore.ActionProviderBase import ActionProviderBase
29 from Products.CMFCore.ActionInformation import ActionInformation
30 from Products.CMFCore.ActionInformation import ActionInformation
31 from OFS.SimpleItem import SimpleItem
32 from AccessControl import ClassSecurityInfo
33 from Globals import InitializeClass
34 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
35
36 import calendar
37 from DateTime import DateTime
38 from sets import Set
39 from types import StringType
40
41 class CalendarTool (UniqueObject, ActionProviderBase, SimpleItem):
42 """ a calendar tool """
43 id = 'portal_calendar'
44 meta_type = 'Plinn Calendar Tool'
45 security = ClassSecurityInfo()
46
47 manage_options = ({ 'label' : 'Configure', 'action' : 'manage_configure' }, ) + \
48 ActionProviderBase.manage_options + \
49 SimpleItem.manage_options
50
51
52
53 #
54 # ZMI methods
55 #
56
57 security.declareProtected( ManagePortal, 'manage_configure' )
58 manage_configure = PageTemplateFile('www/configureCalendarTool', globals(),
59 __name__='manage_configure')
60
61 def __init__(self) :
62 calendar.setfirstweekday(0)
63 self.dateIndexes = ['created', 'modified', 'DateTimeOriginal']
64 self.displayRange = [0, 96]
65 #calViewActionInfo = ActionInformation('calendar_view',
66 # title = 'Calendar View',
67 # category = 'folder',
68 # permissions = (ListFolderContents, ),
69 # condition = 'python: folder is object',
70 # action = 'string: ${folder_url}/calendar_view')
71 self._actions = tuple()
72
73 security.declareProtected(ManagePortal, 'configureTool')
74 def configureTool(self, dateIndexes, displayRange, REQUEST = None) :
75 """ Define date indexes managed by this tool """
76 self.dateIndexes = dateIndexes
77 self.displayRange = map(lambda s : int(s) * 4, displayRange)
78 if REQUEST :
79 return self.manage_configure(self, REQUEST, manage_tabs_message='Saved changes.')
80
81 security.declarePublic('getDateIndexes')
82 def getDateIndexes(self) :
83 """ Return managed date indexes """
84
85 return self.dateIndexes
86
87 security.declareProtected(ManagePortal, 'getCandidateIndexes')
88 def getCandidateIndexes(self) :
89 """ return portal_catalog date and field indexes """
90
91 cTool = getToolByName(self, 'portal_catalog')
92 fIndexes = [index.id for index in cTool.getIndexObjects() if index.meta_type == 'FieldIndex' or \
93 index.meta_type == 'DateIndex' ]
94 fIndexes.sort(lambda a, b : cmp(a.lower(), b.lower()))
95 return fIndexes
96
97 security.declarePublic('getCommonIndexes')
98 def getCommonIndexes(self, objects) :
99 """ Return indexes which belongs to all objects """
100
101 if not objects :
102 return Set([])
103 types = []
104 allIndexSets = []
105 for ob in objects :
106 if ob.meta_type in types :
107 continue
108 else :
109 types.append(ob.meta_type)
110 obIndexes = []
111 for index in self.dateIndexes :
112 if hasattr(ob, index) :
113 obIndexes.append(index)
114 allIndexSets.append(Set(obIndexes))
115 return reduce(lambda a, b : a & b, allIndexSets, Set(self.dateIndexes))
116
117 security.declarePublic('getDisplayRange')
118 def getDisplayRange(self) :
119 """ Return range to display in week view
120 """
121 return self.displayRange
122
123 security.declarePublic('indexIsCallable')
124 def indexIsCallable(self, index, objects = []) :
125 """ Return 1 if callable 0 if not callable or -1 if it's unknown """
126 isCallable = -1
127 if objects :
128 if callable(getattr(objects[0], index)) :
129 isCallable = 1
130 else :
131 isCallable = 0
132 return isCallable
133
134 security.declarePublic('buildDate')
135 def buildDate(self, dateOrString) :
136 """ Return DateTime instance """
137 if type(dateOrString) == StringType :
138 return DateTime(dateOrString)
139 else :
140 return dateOrString
141
142 security.declarePrivate('listActions')
143 def listActions(self, object=None) :
144 """ List action according to indexes """
145
146 actions = list(self._actions)
147
148 if getattr(object, 'isAnObjectManager', False) :
149 request = object.REQUEST
150
151 visible = request['PATH_INFO'].split('/') [-1] == 'calendar_view' and True or False
152 try :
153 if hasattr(object, 'listNearestFolderContents') :
154 objects = object.listNearestFolderContents()
155 elif hasattr(object, 'listFolderContents') :
156 objects = object.listFolderContents()
157 else :
158 objects = object.objectValues()
159 except :
160 objects = []
161
162 if objects :
163 for index in [ index for index in self.getCommonIndexes(objects) ] :
164 ai = ActionInformation( index
165 , title = 'sort_by_index_%s' % index
166 , category = 'additional_tabs'
167 , action = 'string:${folder_url}/calendar_view?sortBy=' + index
168 , visible = visible)
169 actions.append(ai)
170
171 return actions
172
173 security.declarePublic('sortObjectsByDate')
174 def sortObjectsByDate(self, objects, index) :
175 """Sort objects by date index
176 """
177 if objects :
178 if callable(getattr(objects[0], index)) :
179 objects.sort(lambda a, b : cmp(getattr(a, index)(), getattr(b, index)()))
180 else :
181 objects.sort(lambda a, b : cmp(getattr(a, index), getattr(b, index)))
182 return objects
183
184 security.declarePublic('getWeeksList')
185 def getWeeksList(self, objects, index, year=2004, month=5) :
186 """Creates a series of weeks, each of which contains an integer day number.
187 A day number of 0 means that day is in the previous or next month.
188 """
189
190 if objects :
191 getIndexValue = callable(getattr(objects[0], index)) and \
192 ( lambda ob : getattr(ob, index)() ) or \
193 ( lambda ob : getattr(ob, index) )
194 buildDate = type(getIndexValue(objects[0])) == StringType and \
195 ( lambda date : DateTime(date) ) or \
196 ( lambda date : date )
197 weekList = []
198
199 for week in calendar.monthcalendar(year, month) :
200 weekInfoList = []
201
202 for day in week :
203 if day == 0 :
204 inside = []
205 else :
206 inside = []
207 outside = []
208 for ob in objects :
209 obDate = buildDate(getIndexValue(ob))
210 if obDate.year() == year and obDate.month() == month and obDate.day() == day :
211 inside.append(ob)
212 else :
213 outside.append(ob)
214 objects = outside
215
216 dayInfo = {'day' : day,
217 'objects' : inside}
218
219 weekInfoList.append(dayInfo)
220
221 weekList.append(weekInfoList)
222
223 return weekList
224
225 security.declarePublic('getDays')
226 def getDays(self, letters=2):
227 """ Returns a list of days with the correct start day first """
228 return calendar.weekheader(letters).split()
229
230 security.declarePublic('isToday')
231 def isToday(self, year, month, day) :
232 """ return True if date is Today False otherwise
233 """
234 now = DateTime()
235 if now.day() == day and now.month() == month and now.year() == year :
236 return True
237 else :
238 return False
239
240 security.declarePublic('getMonthName')
241 def getMonthName(self, month) :
242 """ return month name """
243 return calendar.month_name[month]
244
245 security.declarePublic('getNextMonth')
246 def getNextMonth(self, year, month) :
247 """ return next month """
248 month += 1
249 if month > 12 :
250 month = 1
251 year += 1
252 return {'year' : year, 'month' : month}
253
254 security.declarePublic('getPreviousMonth')
255 def getPreviousMonth(self, year, month) :
256 """ return previous month """
257 month -= 1
258 if month < 1 :
259 month = 12
260 year -= 1
261 return {'year' : year, 'month' : month}
262
263 security.declarePublic('getWeek')
264 def getWeek(self, objects, index, year=2004, month=5, day=24) :
265 """ return week info """
266
267 weeksList = self.getWeeksList(objects, index, year=year, month=month)
268 for weekIndex in range(len(weeksList)) :
269 if day in [ entry['day'] for entry in weeksList[weekIndex] ] :
270 break
271 week = weeksList[weekIndex]
272
273 for entry in week :
274 entry.update({'month' : month})
275
276 previousWeeksList = None
277 nextWeeksList = None
278
279 if week[0]['day'] == 0 :
280 nbOfDaysInMonthBefore = [ entry['day'] for entry in week ].count(0)
281 previousMonth = self.getPreviousMonth(year, month)
282 previousWeeksList = self.getWeeksList(objects, index, year=previousMonth['year'], month=previousMonth['month'])
283 daysInPreviousMonth = previousWeeksList[-1][:nbOfDaysInMonthBefore]
284 for entry in daysInPreviousMonth :
285 entry.update({'month' : previousMonth['month']})
286
287 daysInThisMonth = week[nbOfDaysInMonthBefore:]
288
289 week = daysInPreviousMonth + daysInThisMonth
290 elif week[-1]['day'] == 0 :
291 nbOfDaysInMonthAfter = [ entry['day'] for entry in week ].count(0)
292 nextMonth = self.getNextMonth(year, month)
293 nextWeeksList = self.getWeeksList(objects, index, year=nextMonth['year'], month=nextMonth['month'])
294 daysInNextMonth = nextWeeksList[0][-nbOfDaysInMonthAfter:]
295 for entry in daysInNextMonth :
296 entry.update({'month' : nextMonth['month']})
297
298 daysInThisMonth = week[:7 - nbOfDaysInMonthAfter]
299
300 week = daysInThisMonth + daysInNextMonth
301
302
303 # previous week
304 if weekIndex > 0 :
305 previousStartDay = {'year' : year,
306 'month' : month,
307 'day' : weeksList[weekIndex - 1][-1]}
308 elif previousWeeksList :
309 previousStartDay = {'year' : previousMonth['year'],
310 'month' : previousMonth['month'],
311 'day' : previousWeeksList[-2][0]}
312 else :
313 # the first week of this month begin on monday
314 previousMonth = self.getPreviousMonth(year, month)
315 previousWeeksList = self.getWeeksList([], index, year=previousMonth['year'], month=previousMonth['month'])
316 previousStartDay = {'year' : previousMonth['year'],
317 'month' : previousMonth['month'],
318 'day' : previousWeeksList[-1][0]}
319
320
321 # next week
322 if weekIndex < len(weeksList) - 1 :
323 nextStartDay = {'year' : year,
324 'month' : month,
325 'day' : weeksList[weekIndex + 1][0]}
326 elif nextWeeksList :
327 nextStartDay = {'year' : nextMonth['year'],
328 'month' : nextMonth['month'],
329 'day' : nextWeeksList[1][0]}
330 else :
331 # the last week of this month ends on sunday
332 nextMonth = self.getNextMonth(year, month)
333 nextWeeksList = self.getWeeksList([], index, year=nextMonth['year'], month=nextMonth['month'])
334 nextStartDay = {'year' : nextMonth['year'],
335 'month' : nextMonth['month'],
336 'day' : nextWeeksList[0][0]}
337
338
339 return {'week' : week,
340 'previousStartDay' : previousStartDay,
341 'nextStartDay' : nextStartDay}
342
343 security.declarePublic('getWeekTable')
344 def getWeekTable(self, week, index, indexIsCallable) :
345 """ Utility method for transposing getWeek result
346 for an easy display in table.
347 """
348
349 weekMatrix = [ [ [] for q in range(96) ] for d in range(7) ]
350
351 getIndexValue = indexIsCallable and \
352 ( lambda ob : getattr(ob, index)() ) or \
353 ( lambda ob : getattr(ob, index) )
354 reelRange = self.displayRange[:]
355 for dayIndex in range(7) :
356 dayInfo = week[dayIndex]
357 for ob in dayInfo['objects'] :
358 date = self.buildDate(getIndexValue(ob))
359
360 minutesAfterMidnight = date.hour() * 60 + date.minute()
361 cellIndex = minutesAfterMidnight / 15
362
363 if cellIndex < reelRange[0] :
364 reelRange[0] = cellIndex
365 elif cellIndex >= reelRange[1] :
366 reelRange[1] = cellIndex + 1
367
368 weekMatrix[dayIndex][cellIndex].append(ob)
369
370 reelRange[0] = reelRange[0] - reelRange[0] % 4
371 reelRange[1] = reelRange[1] + reelRange[1] % 4
372
373 return {'weekMatrix' : weekMatrix, 'range' : reelRange}
374
375
376 security.declarePublic('getEventHeight')
377 def getEventHeight(self, event) :
378 """ Return event height
379 """
380 ee = event.end()
381 es = event.start()
382 days = int( ee - es)
383 if days == 0 :
384 duration = ( ee.hour() * 60 + ee.minute() ) - ( es.hour() * 60 + es.minute() )
385 height = duration / 15
386 return height
387 else :
388 return 1
389 # raise ValueError, "%s event duration is more than 1 day" % event.id
390
391 InitializeClass(CalendarTool)