7a16df70f1130f7109d5a7a4e4e1be3935a80b48
[odoo/odoo.git] / addons / caldav / calendar.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.     
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil import parser
24 from dateutil.rrule import *
25 from osv import osv
26 import re
27 import vobject
28 import common
29
30 # O-1  Optional and can come only once
31 # O-n  Optional and can come more than once
32 # R-1  Required and can come only once
33 # R-n  Required and can come more than once
34
35 def map_data(cr, uid, obj):
36     vals = {}
37     for map_dict in obj.__attribute__:
38         map_val = obj.ical_get(map_dict, 'value')
39         field = obj.ical_get(map_dict, 'field')
40         field_type = obj.ical_get(map_dict, 'type')
41         if field:
42             if field_type == 'selection':
43                 if not map_val:
44                     continue
45                 mapping =obj.__attribute__[map_dict].get('mapping', False)
46                 if mapping:
47                     map_val = mapping[map_val.lower()]
48                 else:
49                     map_val = map_val.lower()
50             if field_type == 'many2many':
51                 ids = []
52                 if not map_val:
53                     vals[field] = ids
54                     continue
55                 model = obj.__attribute__[map_dict].get('object', False)
56                 modobj = obj.pool.get(model)
57                 for map_vall in map_val:
58                     id = modobj.create(cr, uid, map_vall)
59                     ids.append(id)
60                 vals[field] = [(6, 0, ids)]
61                 continue
62             if field_type == 'many2one':
63                 id = None
64                 if not map_val or not isinstance(map_val, dict):
65                     vals[field] = id
66                     continue
67                 model = obj.__attribute__[map_dict].get('object', False)
68                 modobj = obj.pool.get(model)
69                 id = modobj.create(cr, uid, map_val)
70                 vals[field] = id
71                 continue
72             if map_val:
73                 vals[field] = map_val
74     return vals
75
76 class CalDAV(object):
77     __attribute__ = {
78     }
79     def get_recurrent_dates(self, rrulestring, exdate, startdate=None):
80         if not startdate:
81             startdate = datetime.now()         
82         rset1 = rrulestr(rrulestring, dtstart=startdate, forceset=True)
83     
84         for date in exdate:
85             datetime_obj = todate(date)
86             rset1._exdate.append(datetime_obj)
87         re_dates = map(lambda x:x.strftime('%Y-%m-%d %H:%M:%S'), rset1._iter())
88         return re_dates
89
90     def ical_set(self, name, value, type):
91         if name in self.__attribute__ and self.__attribute__[name]:
92             self.__attribute__[name][type] = value
93         return True
94
95     def ical_get(self, name, type):
96         if self.__attribute__.get(name):
97             val = self.__attribute__.get(name).get(type, None)
98             valtype =  self.__attribute__.get(name).get('type', None)
99             if type == 'value':
100                 if valtype and valtype=='datetime' and val:
101                     if isinstance(val, list):
102                         val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
103                     else:
104                         val = val.strftime('%Y-%m-%d %H:%M:%S')
105                 if valtype and valtype=='integer' and val:
106                     val = int(val)
107             return  val
108         else:
109             return  self.__attribute__.get(name, None)
110
111     def ical_reset(self, type):
112         for name in self.__attribute__:
113             if self.__attribute__[name]:
114                 self.__attribute__[name][type] = None
115         return True
116
117     def export_ical(self, cr, uid, datas, vobj=None, context={}):
118         ical = vobject.iCalendar()
119         for data in datas:
120             vevent = ical.add(vobj)
121             for field in self.__attribute__.keys():
122                 map_field = self.ical_get(field, 'field')
123                 map_type = self.ical_get(field, 'type')
124                 if map_field in data.keys():
125                     if field == 'uid' :
126                         model = context.get('model', None)
127                         if not model:
128                             continue
129                         uidval = common.openobjectid2uid(cr, data[map_field], model)
130                         vevent.add('uid').value = uidval
131                     elif field == 'attendee' and data[map_field]:                                                                        
132                         #vevent.add('attendee').value = data[map_field]
133                         #TODO : To export attendee  
134                         pass
135                             
136                     elif field == 'valarm' and data[map_field]:                                                                             
137                         # vevent.add('valarm').value = data[map_field]                        
138                         #TODO : To export valarm 
139                         pass
140
141                     elif data[map_field]:
142                         if map_type == "text":
143                             vevent.add(field).value = str(data[map_field])
144                         elif map_type == 'datetime' and data[map_field]:
145                             if field in ('exdate'):
146                                 vevent.add(field).value = [parser.parse(data[map_field])]
147                             else:
148                                 vevent.add(field).value = parser.parse(data[map_field])
149                         elif map_type == "timedelta":
150                             vevent.add(field).value = timedelta(hours=data[map_field])
151                         elif map_type == "many2one":
152                              vevent.add(field).value = [data.get(map_field)[1]]
153                         if self.__attribute__.get(field).has_key('mapping'):
154                             for key1, val1 in self.ical_get(field, 'mapping').items():
155                                 if val1 == data[map_field]:
156                                     vevent.add(field).value = key1        
157         return ical
158
159     def import_ical(self, cr, uid, ical_data):
160         parsedCal = vobject.readOne(ical_data)
161         att_data = []
162         res = []
163         for child in parsedCal.getChildren():
164             for cal_data in child.getChildren():
165                 #if cal_data.name.lower() == 'attendee':
166                 #    attendee = self.pool.get('basic.calendar.attendee')
167                 #    att_data.append(attendee.import_ical(cr, uid, cal_data))
168                 #    self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
169                 #    continue
170                 #if cal_data.name.lower() == 'valarm':
171                 #    alarm = self.pool.get('basic.calendar.alarm')
172                 #    vals = alarm.import_ical(cr, uid, cal_data)
173                 #    self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
174                 #    continue
175                 if cal_data.name.lower() in self.__attribute__:
176                     self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
177             if child.name.lower() in ('vevent', 'vtodo'):
178                 vals = map_data(cr, uid, self)
179             else:
180                 vals = {}
181                 continue
182             if vals: res.append(vals)
183             self.ical_reset('value')
184         return res
185
186
187 class Calendar(CalDAV, osv.osv_memory):
188     _name = 'basic.calendar'
189     __attribute__ = {
190         'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
191         'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
192                            #             or the minimum and maximum range of the iCalendar specification
193                            #             that is required in order to interpret the iCalendar object.
194         'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
195         'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
196         'vevent': None, # Use: O-n, Type: Collection of Event class
197         'vtodo': None, # Use: O-n, Type: Collection of ToDo class
198         'vjournal': None, # Use: O-n, Type: Collection of Journal class
199         'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
200         'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
201
202     }
203
204 Calendar()
205
206 class Event(CalDAV, osv.osv_memory):
207     _name = 'basic.calendar.event'
208     __attribute__ = {
209         'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar  component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
210         'created': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the calendar information  was created by the calendar user agent in the calendar store.
211         'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
212         'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
213         'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
214         'last-mod': None, # Use: O-1, Type: DATE-TIME        Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
215         'location': None, # Use: O-1, Type: TEXT            Defines the intended venue for the activity defined by a calendar component.
216         'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
217         'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
218         'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
219         'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
220         'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
221         'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
222         'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
223         'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
224         'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
225         'recurid': None, 
226         'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
227         'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
228         'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
229         'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
230         'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a  reference to contact information associated with the calendar component.
231         'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
232         'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
233         'rstatus': None, 
234         'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
235                                 #  like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
236         'resources': None, # Use: O-n, Type: TEXT, Defines the equipment or resources anticipated for an activity specified by a calendar entity like RESOURCES:EASEL,PROJECTOR,VCR, LANGUAGE=fr:1 raton-laveur
237         'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
238         'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
239         'x-prop': None, 
240         'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
241         'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
242     }
243     def export_ical(self, cr, uid, datas, context={}):
244         return super(Event, self).export_ical(cr, uid, datas, 'vevent', context=context)
245     
246 Event()
247
248 class ToDo(CalDAV, osv.osv_memory):
249     _name = 'basic.calendar.todo'
250
251     __attribute__ = {
252                 'class': None, 
253                 'completed': None, 
254                 'created': None, 
255                 'description': None, 
256                 'dtstamp': None, 
257                 'dtstart': None, 
258                 'duration': None,
259                 'due': None,
260                 'geo': None, 
261                 'last-mod ': None, 
262                 'location': None, 
263                 'organizer': None, 
264                 'percent': None, 
265                 'priority': None, 
266                 'recurid': None, 
267                 'seq': None, 
268                 'status': None, 
269                 'summary': None, 
270                 'uid': None, 
271                 'url': None, 
272                 'attach': None, 
273                 'attendee': None, 
274                 'categories': None, 
275                 'comment': None, 
276                 'contact': None, 
277                 'exdate': None, 
278                 'exrule': None, 
279                 'rstatus': None, 
280                 'related': None, 
281                 'resources': None, 
282                 'rdate': None, 
283                 'rrule': None, 
284             }
285     
286     def export_ical(self, cr, uid, datas, context={}):
287         return super(ToDo, self).export_ical(cr, uid, datas, 'vtodo', context=context)
288
289 ToDo()
290
291 class Journal(CalDAV):
292     __attribute__ = {
293     }
294
295 class FreeBusy(CalDAV):
296     __attribute__ = {
297     'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a  reference to contact information associated with the calendar component.
298     'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
299     'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
300     'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
301     'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
302     'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
303     'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
304     'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
305     'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
306     'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
307     'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
308     'rstatus': None, 
309     'X-prop': None, 
310     }
311
312
313 class Timezone(CalDAV):
314     __attribute__ = {
315     'tzid': None, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
316     'last-mod': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
317     'tzurl': None, # Use: O-1, Type: URI, Provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to-date version of itself.
318     'standardc': {'tzprop': None}, # Use: R-1,
319     'daylightc': {'tzprop': None}, # Use: R-1,
320     'x-prop': None, # Use: O-n, Type: Text,
321     }
322
323
324 class Alarm(CalDAV, osv.osv_memory):
325     _name = 'basic.calendar.alarm'
326     __attribute__ = {
327     'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
328     'description': None, #      Type: Text, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. Use:- R-1 for DISPLAY,Use:- R-1 for EMAIL,Use:- R-1 for PROCEDURE
329     'summary': None, # Use: R-1, Type: Text        Which contains the text to be used as the message subject. Use for EMAIL
330     'attendee': None, # Use: R-n, Type: CAL-ADDRESS, Contain the email address of attendees to receive the message. It can also include one or more. Use for EMAIL
331     'trigger': None, # Use: R-1, Type: DURATION, The "TRIGGER" property specifies a duration prior to the start of an event or a to-do. The "TRIGGER" edge may be explicitly set to be relative to the "START" or "END" of the event or to-do with the "related" parameter of the "TRIGGER" property. The "TRIGGER" property value type can alternatively be set to an absolute calendar date and time of day value. Use for all action like AUDIO, DISPLAY, EMAIL and PROCEDURE
332     'duration': None, #           Type: DURATION, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
333     'repeat': None, #           Type: INTEGER, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
334     'attach': None, # Use:- O-n: which MUST point to a sound resource, which is rendered when the alarm is triggered for AUDIO, Use:- O-n: which are intended to be sent as message attachments for EMAIL, Use:- R-1:which MUST point to a procedure resource, which is invoked when the alarm is triggered for PROCEDURE.
335     'x-prop': None, 
336     }
337
338     def export_ical(self, cr, uid, alarm_datas, context={}):
339         ical = vobject.iCalendar()
340         vevent = ical.add('vevent')
341         valarms = []
342         for alarm_data in alarm_datas:
343             valarm = vevent.add('valarm')
344             
345             # Compute trigger data
346             interval = alarm_data['trigger_interval']
347             occurs = alarm_data['trigger_occurs']
348             duration = (occurs == 'after' and alarm_data['trigger_duration']) \
349                                                 or -(alarm_data['trigger_duration'])
350             related = alarm_data['trigger_related']
351             trigger = valarm.add('trigger')
352             trigger.params['related'] = [related.upper()]
353             if interval == 'days':
354                 delta = timedelta(days=duration)
355             if interval == 'hours':
356                 delta = timedelta(hours=duration)
357             if interval == 'minutes':
358                 delta = timedelta(minutes=duration)
359             trigger.value = delta
360
361             # Compute other details
362             valarm.add('description').value = alarm_data['name']
363             valarm.add('action').value = alarm_data['action']
364             
365             valarms.append(valarm)
366         return valarms
367         
368     def import_ical(self, cr, uid, ical_data):        
369         for child in ical_data.getChildren():
370             if child.name.lower() == 'trigger':
371                 seconds = child.value.seconds
372                 days = child.value.days
373                 diff = (days * 86400) +  seconds
374                 interval = 'days'
375                 related = 'before'
376                 if not seconds:
377                     duration = abs(days)
378                     related = days>0 and 'after' or 'before'
379                 elif (abs(diff) / 3600) == 0:
380                     duration = abs(diff / 60)
381                     interval = 'minutes'
382                     related = days>=0 and 'after' or 'before'
383                 else:
384                     duration = abs(diff / 3600)
385                     interval = 'hours'
386                     related = days>=0 and 'after' or 'before'
387                 self.ical_set('trigger_interval', interval, 'value')
388                 self.ical_set('trigger_duration', duration, 'value')
389                 self.ical_set('trigger_occurs', related.lower(), 'value')
390                 if child.params:
391                     if child.params.get('related'):
392                         self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
393             else:
394                 self.ical_set(child.name.lower(), child.value.lower(), 'value')
395         vals = map_data(cr, uid, self)
396         return vals
397
398 Alarm()
399
400 class Attendee(CalDAV, osv.osv_memory):
401     _name = 'basic.calendar.attendee'
402     __attribute__ = {
403     'cutype': None, # Use: 0-1    Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
404     'member': None, # Use: 0-1    Specify the group or list membership of the calendar user specified by the property.
405     'role': None, # Use: 0-1    Specify the participation role for the calendar user specified by the property like "CHAIR"/"REQ-PARTICIPANT"/"OPT-PARTICIPANT"/"NON-PARTICIPANT"
406     'partstat': None, # Use: 0-1    Specify the participation status for the calendar user specified by the property. like use for VEVENT:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED", use for VTODO:-"NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED"/"COMPLETED"/"IN-PROCESS" and use for VJOURNAL:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED".
407     'rsvp': None, # Use: 0-1    Specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value like TRUE / FALSE.
408     'delegated-to': None, # Use: 0-1    Specify the calendar users to whom the calendar user specified by the property has delegated participation.
409     'delegated-from': None, # Use: 0-1    Specify the calendar users that have delegated their participation to the calendar user specified by the property.
410     'sent-by': None, # Use: 0-1    Specify the calendar user that is acting on behalf of the calendar user specified by the property.
411     'cn': None, # Use: 0-1    Specify the common name to be associated with the calendar user specified by the property.
412     'dir': None, # Use: 0-1    Specify reference to a directory entry associated with the calendar user specified by the property.
413     'language': None, # Use: 0-1    Specify the language for text values in a property or property parameter.
414     }
415
416     def import_ical(self, cr, uid, ical_data):        
417         for para in ical_data.params:
418             if para.lower() == 'cn':
419                 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ ical_data.value, 'value')
420             else:
421                 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
422         if not ical_data.params.get('CN'):
423             self.ical_set('cn', ical_data.value, 'value')
424         vals = map_data(cr, uid, self)
425         return vals
426
427     def export_ical(self, cr, uid, attendee_data, context={}):        
428         ical = vobject.iCalendar()
429         attendees = []        
430         vevent = ical.add('vevent')
431         for attendee in attendee_data:            
432             attendee_add = vevent.add('attendee')
433             for a_key, a_val in self.__attribute__.items():                
434                 if attendee[a_val['field']]:
435                     if a_val['type'] == 'text':
436                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
437                     elif a_val['type'] == 'boolean':
438                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
439             attendees.append(attendee_add)
440         return attendees
441
442 Attendee()
443
444
445 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: