[ADD]: Images: caldav, crm_caldav, document_webdav, project_caldav
[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
27 try:
28     from tools.dict_tools import  dict_merge2
29 except ImportError:
30     from document.dict_tools import  dict_merge2
31
32 # TODO: implement DAV-aware errors, inherit from IOError
33
34 # Assuming that we have set global properties right, we mark *all* 
35 # directories as having calendar-access.
36 nodes.node_dir.http_options = dict_merge2(nodes.node_dir.http_options,
37             { 'DAV': ['calendar-access',] })
38
39 class node_calendar_collection(nodes.node_dir):
40     DAV_PROPS = dict_merge2(nodes.node_dir.DAV_PROPS,
41             { "http://calendarserver.org/ns/" : ('getctag',), } )
42     DAV_M_NS = dict_merge2(nodes.node_dir.DAV_M_NS,
43             { "http://calendarserver.org/ns/" : '_get_dav', } )
44
45     def _file_get(self,cr, nodename=False):
46         return []
47
48     def _child_get(self, cr, name=False, parent_id=False, domain=None):
49         dirobj = self.context._dirobj
50         uid = self.context.uid
51         ctx = self.context.context.copy()
52         ctx.update(self.dctx)
53         where = [('collection_id','=',self.dir_id)]
54         ext = False
55         if name and name.endswith('.ics'):
56             name = name[:-4]
57             ext = True
58         if name:
59             where.append(('name','=',name))
60         if not domain:
61             domain = []
62         where = where + domain
63         fil_obj = dirobj.pool.get('basic.calendar')
64         ids = fil_obj.search(cr,uid,where,context=ctx)
65         res = []
66         for cal in fil_obj.browse(cr, uid, ids, context=ctx):
67             if (not name) or not ext:
68                 res.append(node_calendar(cal.name, self, self.context, cal))
69             if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
70                 # these ones must not see the webcal entry.
71                 continue
72             if cal.has_webcal and (not name) or ext:
73                 res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal))
74             # May be both of them!
75         return res
76
77     def _get_ttag(self, cr):
78         return 'calen-dir-%d' % self.dir_id
79
80     def _get_dav_getctag(self, cr):
81         dirobj = self.context._dirobj
82         uid = self.context.uid
83         ctx = self.context.context.copy()
84         ctx.update(self.dctx)
85         where = [('collection_id','=',self.dir_id)]
86         bc_obj = dirobj.pool.get('basic.calendar')
87         
88         res = get_last_modified(bc_obj, cr, uid, where, context=ctx)
89         return _str2time(res)
90
91 class node_calendar_res_col(nodes.node_res_obj):
92     """ Calendar collection, as a dynamically created node
93     
94     This class shall be used instead of node_calendar_collection, when the
95     node is under dynamic ones.
96     """
97     DAV_PROPS = dict_merge2(nodes.node_res_obj.DAV_PROPS,
98             { "http://calendarserver.org/ns/" : ('getctag',), } )
99     DAV_M_NS = dict_merge2(nodes.node_res_obj.DAV_M_NS,
100             { "http://calendarserver.org/ns/" : '_get_dav', } )
101
102     def _file_get(self,cr, nodename=False):
103         return []
104
105     def _child_get(self, cr, name=False, parent_id=False, domain=None):
106         dirobj = self.context._dirobj
107         uid = self.context.uid
108         ctx = self.context.context.copy()
109         ctx.update(self.dctx)
110         where = [('collection_id','=',self.dir_id)]
111         ext = False
112         if name and name.endswith('.ics'):
113             name = name[:-4]
114             ext = True
115         if name:
116             where.append(('name','=',name))
117         if not domain:
118             domain = []
119         where = where + domain
120         fil_obj = dirobj.pool.get('basic.calendar')
121         ids = fil_obj.search(cr,uid,where,context=ctx)
122         res = []
123         # TODO: shall we use any of our dynamic information??
124         for cal in fil_obj.browse(cr, uid, ids, context=ctx):
125             if (not name) or not ext:
126                 res.append(node_calendar(cal.name, self, self.context, cal))
127             if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
128                 # these ones must not see the webcal entry.
129                 continue
130             if cal.has_webcal and (not name) or ext:
131                 res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal))
132             # May be both of them!
133         return res
134
135     def _get_ttag(self, cr):
136         return 'calen-dir-%d' % self.dir_id
137
138     def _get_dav_getctag(self, cr):
139         dirobj = self.context._dirobj
140         uid = self.context.uid
141         ctx = self.context.context.copy()
142         ctx.update(self.dctx)
143         where = [('collection_id','=',self.dir_id)]
144         bc_obj = dirobj.pool.get('basic.calendar')
145         
146         res = get_last_modified(bc_obj, cr, uid, where, context=ctx)
147         return _str2time(res)
148
149 class node_calendar(nodes.node_class):
150     our_type = 'collection'
151     DAV_PROPS = {
152             "DAV:": ('supported-report-set',),
153             # "http://cal.me.com/_namespace/" : ('user-state',),
154             "http://calendarserver.org/ns/" : ( 'getctag',),
155             'http://groupdav.org/': ('resourcetype',),
156             "urn:ietf:params:xml:ns:caldav" : (
157                     'calendar-description', 
158                     'supported-calendar-component-set',
159                     ),
160             "http://apple.com/ns/ical/": ("calendar-color", "calendar-order"),
161             }
162     DAV_PROPS_HIDDEN = {
163             "urn:ietf:params:xml:ns:caldav" : (
164                     'calendar-data',
165                     'calendar-timezone',
166                     'supported-calendar-data',
167                     'max-resource-size',
168                     'min-date-time',
169                     'max-date-time',
170                     )}
171
172     DAV_M_NS = {
173            "DAV:" : '_get_dav',
174            # "http://cal.me.com/_namespace/": '_get_dav', 
175            'http://groupdav.org/': '_get_gdav',
176            "http://calendarserver.org/ns/" : '_get_dav',
177            "urn:ietf:params:xml:ns:caldav" : '_get_caldav',
178            "http://apple.com/ns/ical/": '_get_apple_cal',
179            }
180
181     http_options = { 'DAV': ['calendar-access'] }
182
183     def __init__(self,path, parent, context, calendar):
184         super(node_calendar,self).__init__(path, parent,context)
185         self.calendar_id = calendar.id
186         self.mimetype = 'application/x-directory'
187         self.create_date = calendar.create_date
188         self.write_date = calendar.write_date or calendar.create_date
189         self.content_length = 0
190         self.displayname = calendar.name
191         self.cal_type = calendar.type
192         self.cal_color = calendar.calendar_color or None
193         self.cal_order = calendar.calendar_order or None
194         try:
195             self.uuser = (calendar.user_id and calendar.user_id.login) or 'nobody'
196         except Exception:
197             self.uuser = 'nobody'
198
199     def _get_dav_getctag(self, cr):
200         dirobj = self.context._dirobj
201         uid = self.context.uid
202         ctx = self.context.context.copy()
203         ctx.update(self.dctx)
204
205         bc_obj = dirobj.pool.get('basic.calendar')
206         res = bc_obj.get_cal_max_modified(cr, uid, [self.calendar_id], self, domain=[], context=ctx)
207         return _str2time(res)
208
209     def _get_dav_user_state(self, cr):
210         #TODO
211         return 'online'
212
213     def get_dav_resourcetype(self, cr):
214         res = [ ('collection', 'DAV:'),
215                 ('calendar', 'urn:ietf:params:xml:ns:caldav'),
216                 ]
217         if self.context.get('DAV-client', '') == 'GroupDAV':
218             res.append((str(self.cal_type + '-collection'), 'http://groupdav.org/'))
219         return res
220
221     def get_domain(self, cr, filters):
222         # TODO: doc.
223         res = []
224         if not filters:
225             return res
226         _log = logging.getLogger('caldav.query')
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
328         if res and len(res):
329             # We arbitrarily construct only the first node of the data
330             # that have been imported. ICS may have had more elements,
331             # but only one node can be returned here.
332             assert isinstance(res[0], (int, long))
333             fnodes = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self,
334                     domain=[('id','=',res[0])], context=ctx)
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