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