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