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