Document, calendar: Patch code from trunk-xrg branch
[odoo/odoo.git] / addons / caldav / caldav_node.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 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 import time
23 from document_webdav import nodes
24 from document.nodes import _str2time
25 import logging
26 import StringIO
27 from orm_utils import get_last_modified
28
29 from tools.dict_tools import dict_merge, dict_merge2
30
31 # TODO: implement DAV-aware errors, inherit from IOError
32
33 # Assuming that we have set global properties right, we mark *all* 
34 # directories as having calendar-access.
35 nodes.node_dir.http_options = dict_merge2(nodes.node_dir.http_options,
36             { 'DAV': ['calendar-access',] })
37
38 class node_calendar_collection(nodes.node_dir):
39     DAV_PROPS = dict_merge2(nodes.node_dir.DAV_PROPS,
40             { "http://calendarserver.org/ns/" : ('getctag',), } )
41     DAV_M_NS = dict_merge2(nodes.node_dir.DAV_M_NS,
42             { "http://calendarserver.org/ns/" : '_get_dav', } )
43
44     def _file_get(self,cr, nodename=False):
45         return []
46
47     def _child_get(self, cr, name=False, parent_id=False, domain=None):
48         dirobj = self.context._dirobj
49         uid = self.context.uid
50         ctx = self.context.context.copy()
51         ctx.update(self.dctx)
52         where = [('collection_id','=',self.dir_id)]
53         ext = False
54         if name and name.endswith('.ics'):
55             name = name[:-4]
56             ext = True
57         if name:
58             where.append(('name','=',name))
59         if not domain:
60             domain = []
61         where = where + domain
62         fil_obj = dirobj.pool.get('basic.calendar')
63         ids = fil_obj.search(cr,uid,where,context=ctx)
64         res = []
65         for cal in fil_obj.browse(cr, uid, ids, context=ctx):
66             if (not name) or not ext:
67                 res.append(node_calendar(cal.name, self, self.context, cal))
68             if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
69                 # these ones must not see the webcal entry.
70                 continue
71             if cal.has_webcal and (not name) or ext:
72                 res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal))
73             # May be both of them!
74         return res
75
76     def _get_ttag(self, cr):
77         return 'calen-dir-%d' % self.dir_id
78
79     def _get_dav_getctag(self, cr):
80         dirobj = self.context._dirobj
81         uid = self.context.uid
82         ctx = self.context.context.copy()
83         ctx.update(self.dctx)
84         where = [('collection_id','=',self.dir_id)]
85         bc_obj = dirobj.pool.get('basic.calendar')
86         
87         res = get_last_modified(bc_obj, cr, uid, where, context=ctx)
88         return _str2time(res)
89
90 class node_calendar_res_col(nodes.node_res_obj):
91     """ Calendar collection, as a dynamically created node
92     
93     This class shall be used instead of node_calendar_collection, when the
94     node is under dynamic ones.
95     """
96     DAV_PROPS = dict_merge2(nodes.node_res_obj.DAV_PROPS,
97             { "http://calendarserver.org/ns/" : ('getctag',), } )
98     DAV_M_NS = dict_merge2(nodes.node_res_obj.DAV_M_NS,
99             { "http://calendarserver.org/ns/" : '_get_dav', } )
100
101     def _file_get(self,cr, nodename=False):
102         return []
103
104     def _child_get(self, cr, name=False, parent_id=False, domain=None):
105         dirobj = self.context._dirobj
106         uid = self.context.uid
107         ctx = self.context.context.copy()
108         ctx.update(self.dctx)
109         where = [('collection_id','=',self.dir_id)]
110         ext = False
111         if name and name.endswith('.ics'):
112             name = name[:-4]
113             ext = True
114         if name:
115             where.append(('name','=',name))
116         if not domain:
117             domain = []
118         where = where + domain
119         fil_obj = dirobj.pool.get('basic.calendar')
120         ids = fil_obj.search(cr,uid,where,context=ctx)
121         res = []
122         # TODO: shall we use any of our dynamic information??
123         for cal in fil_obj.browse(cr, uid, ids, context=ctx):
124             if (not name) or not ext:
125                 res.append(node_calendar(cal.name, self, self.context, cal))
126             if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
127                 # these ones must not see the webcal entry.
128                 continue
129             if cal.has_webcal and (not name) or ext:
130                 res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal))
131             # May be both of them!
132         return res
133
134     def _get_ttag(self, cr):
135         return 'calen-dir-%d' % self.dir_id
136
137     def _get_dav_getctag(self, cr):
138         dirobj = self.context._dirobj
139         uid = self.context.uid
140         ctx = self.context.context.copy()
141         ctx.update(self.dctx)
142         where = [('collection_id','=',self.dir_id)]
143         bc_obj = dirobj.pool.get('basic.calendar')
144         
145         res = get_last_modified(bc_obj, cr, uid, where, context=ctx)
146         return _str2time(res)
147
148 class node_calendar(nodes.node_class):
149     our_type = 'collection'
150     DAV_PROPS = {
151             "DAV:": ('supported-report-set',),
152             # "http://cal.me.com/_namespace/" : ('user-state',),
153             "http://calendarserver.org/ns/" : ( 'getctag',),
154             'http://groupdav.org/': ('resourcetype',),
155             "urn:ietf:params:xml:ns:caldav" : (
156                     'calendar-description', 
157                     'supported-calendar-component-set',
158                     ),
159             "http://apple.com/ns/ical/": ("calendar-color", "calendar-order"),
160             }
161     DAV_PROPS_HIDDEN = {
162             "urn:ietf:params:xml:ns:caldav" : (
163                     'calendar-data',
164                     'calendar-timezone',
165                     'supported-calendar-data',
166                     'max-resource-size',
167                     'min-date-time',
168                     'max-date-time',
169                     )}
170
171     DAV_M_NS = {
172            "DAV:" : '_get_dav',
173            # "http://cal.me.com/_namespace/": '_get_dav', 
174            'http://groupdav.org/': '_get_gdav',
175            "http://calendarserver.org/ns/" : '_get_dav',
176            "urn:ietf:params:xml:ns:caldav" : '_get_caldav',
177            "http://apple.com/ns/ical/": '_get_apple_cal',
178            }
179
180     http_options = { 'DAV': ['calendar-access'] }
181
182     def __init__(self,path, parent, context, calendar):
183         super(node_calendar,self).__init__(path, parent,context)
184         self.calendar_id = calendar.id
185         self.mimetype = 'application/x-directory'
186         self.create_date = calendar.create_date
187         self.write_date = calendar.write_date or calendar.create_date
188         self.content_length = 0
189         self.displayname = calendar.name
190         self.cal_type = calendar.type
191         self.cal_color = calendar.calendar_color or None
192         self.cal_order = calendar.calendar_order or None
193         try:
194             self.uuser = (calendar.user_id and calendar.user_id.login) or 'nobody'
195         except Exception:
196             self.uuser = 'nobody'
197
198     def _get_dav_getctag(self, cr):
199         dirobj = self.context._dirobj
200         uid = self.context.uid
201         ctx = self.context.context.copy()
202         ctx.update(self.dctx)
203
204         bc_obj = dirobj.pool.get('basic.calendar')
205         res = bc_obj.get_cal_max_modified(cr, uid, [self.calendar_id], self, domain=[], context=ctx)
206         return _str2time(res)
207
208     def _get_dav_user_state(self, cr):
209         #TODO
210         return 'online'
211
212     def get_dav_resourcetype(self, cr):
213         res = [ ('collection', 'DAV:'),
214                 ('calendar', 'urn:ietf:params:xml:ns:caldav'),
215                 ]
216         if self.context.get('DAV-client', '') == 'GroupDAV':
217             res.append((str(self.cal_type + '-collection'), 'http://groupdav.org/'))
218         return res
219
220     def get_domain(self, cr, filters):
221         # TODO: doc.
222         res = []
223         if not filters:
224             return res
225         _log = logging.getLogger('caldav.query')
226         if filters.localName == 'calendar-query':
227             res = []
228             for filter_child in filters.childNodes:
229                 if filter_child.nodeType == filter_child.TEXT_NODE:
230                     continue
231                 if filter_child.localName == 'filter':
232                     for vcalendar_filter in filter_child.childNodes:
233                         if vcalendar_filter.nodeType == vcalendar_filter.TEXT_NODE:
234                             continue
235                         if vcalendar_filter.localName == 'comp-filter':
236                             if vcalendar_filter.getAttribute('name') == 'VCALENDAR':
237                                 for vevent_filter in vcalendar_filter.childNodes:
238                                     if vevent_filter.nodeType == vevent_filter.TEXT_NODE:
239                                         continue
240                                     if vevent_filter.localName == 'comp-filter':
241                                         if vevent_filter.getAttribute('name'):
242                                             res = [('type','=',vevent_filter.getAttribute('name').lower() )]
243                                             
244                                         for cfe in vevent_filter.childNodes:
245                                             if cfe.localName == 'time-range':
246                                                 if cfe.getAttribute('start'):
247                                                     _log.warning("Ignore start.. ")
248                                                     # No, it won't work in this API
249                                                     #val = cfe.getAttribute('start')
250                                                     #res += [('dtstart','=', cfe)]
251                                                 elif cfe.getAttribute('end'):
252                                                     _log.warning("Ignore end.. ")
253                                             else:
254                                                 _log.debug("Unknown comp-filter: %s", cfe.localName)
255                                     else:
256                                         _log.debug("Unknown comp-filter: %s", vevent_filter.localName)
257                         else:
258                             _log.debug("Unknown filter element: %s", vcalendar_filter.localName)
259                 else:
260                     _log.debug("Unknown calendar-query element: %s", filter_child.localName)
261             return res
262         elif filters.localName == 'calendar-multiget':
263             # this is not the place to process, as it wouldn't support multi-level
264             # hrefs. So, the code is moved to document_webdav/dav_fs.py
265             pass
266         else:
267             _log.debug("Unknown element in REPORT: %s", filters.localName)
268         return res
269
270     def children(self, cr, domain=None):
271         return self._child_get(cr, domain=domain)
272
273     def child(self,cr, name, domain=None):
274         res = self._child_get(cr, name, domain=domain)
275         if res:
276             return res[0]
277         return None
278
279
280     def _child_get(self, cr, name=False, parent_id=False, domain=None):
281         dirobj = self.context._dirobj
282         uid = self.context.uid
283         ctx = self.context.context.copy()
284         ctx.update(self.dctx)
285         where = []
286         if name:
287             if name.endswith('.ics'):
288                 name = name[:-4]
289             try:
290                 where.append(('id','=',int(name)))
291             except ValueError:
292                 # if somebody requests any other name than the ones we
293                 # generate (non-numeric), it just won't exist
294                 # FIXME: however, this confuses Evolution (at least), which
295                 # thinks the .ics node hadn't been saved.
296                 return []
297
298         if not domain:
299             domain = []
300
301         bc_obj = dirobj.pool.get('basic.calendar')
302         # we /could/ be supplying an invalid calendar id to bc_obj, it has to check
303         res = bc_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=where, context=ctx)
304         return res
305
306     def create_child(self, cr, path, data):
307         """ API function to create a child file object and node
308             Return the node_* created
309         """
310         # we ignore the path, it will be re-generated automatically
311         fil_obj = self.context._dirobj.pool.get('basic.calendar')
312         ctx = self.context.context.copy()
313         ctx.update(self.dctx)
314         uid = self.context.uid
315
316         res = self.set_data(cr, data)
317
318         if res and len(res):
319             # We arbitrarily construct only the first node of the data
320             # that have been imported. ICS may have had more elements,
321             # but only one node can be returned here.
322             assert isinstance(res[0], (int, long))
323             fnodes = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self,
324                     domain=[('id','=',res[0])], context=ctx)
325             return fnodes[0]
326         # If we reach this line, it means that we couldn't import any useful
327         # (and matching type vs. our node kind) data from the iCal content.
328         return None
329
330
331     def set_data(self, cr, data, fil_obj = None):
332         uid = self.context.uid
333         calendar_obj = self.context._dirobj.pool.get('basic.calendar')
334         res = calendar_obj.import_cal(cr, uid, data, self.calendar_id)
335         return res
336
337     def get_data_len(self, cr, fil_obj = None):
338         return self.content_length
339
340     def _get_ttag(self,cr):
341         return 'calendar-%d' % (self.calendar_id,)
342
343     def rmcol(self, cr):
344         return False
345
346     def _get_caldav_calendar_data(self, cr):
347         if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
348             # Never return collective data to iClients, they get confused
349             # because they do propfind on the calendar node with Depth=1
350             # and only expect the childrens' data
351             return None
352         res = []
353         for child in self.children(cr):
354             res.append(child._get_caldav_calendar_data(cr))
355         return res
356
357     def _get_caldav_calendar_description(self, cr):
358         uid = self.context.uid
359         calendar_obj = self.context._dirobj.pool.get('basic.calendar')
360         ctx = self.context.context.copy()
361         ctx.update(self.dctx)
362         try:
363             calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
364             return calendar.description or calendar.name
365         except Exception, e:
366             return None
367
368     def _get_dav_supported_report_set(self, cr):
369         
370         return ('supported-report', 'DAV:', 
371                     ('report','DAV:',
372                             ('principal-match','DAV:')
373                     )
374                 )
375
376     def _get_caldav_supported_calendar_component_set(self, cr):
377         return ('comp', 'urn:ietf:params:xml:ns:caldav', None,
378                     {'name': self.cal_type.upper()} )
379         
380     def _get_caldav_calendar_timezone(self, cr):
381         return None #TODO
382         
383     def _get_caldav_supported_calendar_data(self, cr):
384         return ('calendar-data', 'urn:ietf:params:xml:ns:caldav', None,
385                     {'content-type': "text/calendar", 'version': "2.0" } )
386         
387     def _get_caldav_max_resource_size(self, cr):
388         return 65535
389
390     def _get_caldav_min_date_time(self, cr):
391         return "19700101T000000Z"
392
393     def _get_caldav_max_date_time(self, cr):
394         return "21001231T235959Z" # I will be dead by then
395     
396     def _get_apple_cal_calendar_color(self, cr):
397         return self.cal_color
398
399     def _get_apple_cal_calendar_order(self, cr):
400         return self.cal_order
401
402 class res_node_calendar(nodes.node_class):
403     our_type = 'file'
404     DAV_PROPS = {
405             "http://calendarserver.org/ns/" : ('getctag',),
406             "urn:ietf:params:xml:ns:caldav" : (
407                     'calendar-description',
408                     'calendar-data',
409                     )}
410     DAV_M_NS = {
411            "http://calendarserver.org/ns/" : '_get_dav',
412            "urn:ietf:params:xml:ns:caldav" : '_get_caldav'}
413
414     http_options = { 'DAV': ['calendar-access'] }
415
416     def __init__(self,path, parent, context, res_obj, res_model=None, res_id=None):
417         super(res_node_calendar,self).__init__(path, parent, context)
418         self.mimetype = 'text/calendar'
419         self.create_date = parent.create_date
420         self.write_date = parent.write_date or parent.create_date
421         self.calendar_id = hasattr(parent, 'calendar_id') and parent.calendar_id or False
422         if res_obj:
423             if not self.calendar_id: self.calendar_id = res_obj.id
424             pr = res_obj.perm_read(context=context, details=False)[0]
425             self.create_date = pr.get('create_date')
426             self.write_date = pr.get('write_date') or pr.get('create_date')
427             self.displayname = res_obj.name
428
429         self.content_length = 0
430
431         self.model = res_model
432         self.res_id = res_id
433
434     def open(self, cr, mode=False):
435         if self.type in ('collection','database'):
436             return False
437         s = StringIO.StringIO(self.get_data(cr))
438         s.name = self
439         return s
440
441     def get_data(self, cr, fil_obj = None):
442         uid = self.context.uid
443         calendar_obj = self.context._dirobj.pool.get('basic.calendar')
444         context = self.context.context.copy()
445         context.update(self.dctx)
446         context.update({'model': self.model, 'res_id':self.res_id})
447         res = calendar_obj.export_cal(cr, uid, [self.calendar_id], context=context)
448         return res
449   
450     def _get_caldav_calendar_data(self, cr):
451         return self.get_data(cr)
452
453     def get_data_len(self, cr, fil_obj = None):
454         return self.content_length
455
456     def set_data(self, cr, data, fil_obj = None):
457         uid = self.context.uid
458         context = self.context.context.copy()
459         context.update(self.dctx)
460         context.update({'model': self.model, 'res_id':self.res_id})
461         calendar_obj = self.context._dirobj.pool.get('basic.calendar')
462         res =  calendar_obj.import_cal(cr, uid, data, self.calendar_id, context=context)
463         return res
464
465     def _get_ttag(self,cr):
466         res = False
467         if self.model and self.res_id:
468             res = '%s_%d' % (self.model, self.res_id)
469         elif self.calendar_id:
470             res = '%d' % (self.calendar_id)
471         return res
472
473     def rm(self, cr):
474         uid = self.context.uid
475         res = False
476         if self.type in ('collection','database'):
477             return False
478         if self.model and self.res_id:
479             document_obj = self.context._dirobj.pool.get(self.model)
480             if document_obj:
481                 res =  document_obj.unlink(cr, uid, [self.res_id])
482
483         return res
484
485    
486
487 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4