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