├── README.md
└── document_caldav
├── __init__.py
├── static
└── src
│ ├── xml
│ └── base.xml
│ └── js
│ └── search.js
├── calendar_collection.py
├── __openerp__.py
├── caldav_setup.xml
└── caldav_node.py
/README.md:
--------------------------------------------------------------------------------
1 | # odoo-dav
2 |
--------------------------------------------------------------------------------
/document_caldav/__init__.py:
--------------------------------------------------------------------------------
1 | from . import calendar_collection
2 |
--------------------------------------------------------------------------------
/document_caldav/static/src/xml/base.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
CalDAV url
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/document_caldav/static/src/js/search.js:
--------------------------------------------------------------------------------
1 | openerp.document_caldav = function(instance){
2 | var module = instance.web.search;
3 |
4 | module.CustomFilters.include({
5 | toggle_filter: function (filter, preventSearch) {
6 | this._super(filter, preventSearch);
7 | var current_id = this.view.query.pluck('_id');
8 | var url = '';
9 | if (current_id.length) {
10 | url = this.session.server + '/webdav/' +
11 | this.session.db + '/calendar/users/' +
12 | this.session.username + '/a/m-' + this.view.model +
13 | '/filtered-' + current_id[0] + '/';
14 | }
15 | $('#filter_caldav_url').val(url);
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/document_caldav/calendar_collection.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ##############################################################################
3 | #
4 | # OpenERP, Open Source Management Solution
5 | # Copyright (C) 2013-2016 Vertel AB ().
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 .
19 | #
20 | ##############################################################################
21 |
22 | from openerp import models, fields, api, _
23 | from openerp.exceptions import except_orm, Warning, RedirectWarning
24 |
25 | import logging
26 | _logger = logging.getLogger(__name__)
27 |
28 | from .caldav_node import node_model_calendar_collection
29 |
30 | class document_directory(models.Model):
31 | _inherit = 'document.directory'
32 |
33 | calendar_collection = fields.Boolean('Calendar Collection',default=False)
34 |
35 | def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
36 | if dbro is None:
37 | dbro = self.browse(cr, uid, ids, context=context)
38 |
39 | if dbro.calendar_collection:
40 | return node_model_calendar_collection
41 | else:
42 | return super(document_directory, self).get_node_class(cr, uid, ids, dbro=dbro, dynamic=dynamic,context=context)
43 |
44 | #~ def get_description(self, cr, uid, ids, context=None):
45 | #~ # TODO : return description of all calendars
46 | #~ return False
47 |
48 |
49 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
50 |
--------------------------------------------------------------------------------
/document_caldav/__openerp__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ##############################################################################
3 | #
4 | # OpenERP, Open Source Management Solution
5 | # Copyright (C) 2010-2013 OpenERP s.a. ().
6 | # Copyright (C) 2013-2016 Vertel AB
7 | #
8 | # This program is free software: you can redistribute it and/or modify
9 | # it under the terms of the GNU Affero General Public License as
10 | # published by the Free Software Foundation, either version 3 of the
11 | # License, or (at your option) any later version.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU Affero General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU Affero General Public License
19 | # along with this program. If not, see .
20 | #
21 | ##############################################################################
22 | {
23 | "name": "CalDAV",
24 | "version": "0.1",
25 | "depends": ["base", "document_webdav_fast", "web"],
26 | 'author': 'Vertel AB',
27 | "category": "",
28 | "summary": "A simple CalDAV implementation",
29 | 'license': 'AGPL-3',
30 | "description": """
31 | A very simple CalDAV implementation inspired of InitOS Carddav implementation
32 | ====================================
33 |
34 | Urls:
35 |
36 | - For a partners calendar: /webdav/$db_name/calendar/users/$login/a/$partner_name/
37 |
38 | Collections can be filtered, the url is then shown in the search view drop-down.
39 | # sudo pip install pywebdav
40 | # sudo pip install icalendar
41 | # document_webdav_fast from https://github.com/initOS/openerp-dav
42 | """,
43 | 'data': [
44 | 'caldav_setup.xml',
45 | ],
46 | 'demo': [
47 | ],
48 | 'external_dependenies': {'python': ['pywebdav','icalendar'],'bin': []},
49 | 'test': [
50 | ],
51 | 'installable': True,
52 | 'auto_install': False,
53 | 'js': ['static/src/js/search.js'],
54 | 'qweb': ['static/src/xml/base.xml'],
55 | }
56 |
--------------------------------------------------------------------------------
/document_caldav/caldav_setup.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | []
7 |
8 |
9 |
10 |
11 | directory
12 |
13 | calendar
14 |
15 |
16 | [('id','=',uid)]
17 |
18 |
19 |
20 |
21 |
22 |
23 | ressource
24 |
25 |
26 | users
27 |
28 |
29 | []
30 |
31 |
32 |
33 |
34 | directory
35 |
36 | a
37 |
38 |
39 |
40 |
41 | urn:ietf:params:xml:ns:caldav
42 |
43 | calendar-home-set
44 | ('href','DAV:','/%s/%s/calendar/users/%s/a' % ('webdav',dbname,username) )
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/document_caldav/caldav_node.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ##############################################################################
3 | #
4 | # OpenERP, Open Source Management Solution
5 | # Copyright (C) 2004-2010 Odoo SA
6 | # Copyright (C) 2013-2016 Vertel AB.
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 .
19 | #
20 | ##############################################################################
21 | # sudo pip install pywebdav
22 | # document_webdav_fast from https://github.com/initOS/openerp-dav
23 | from openerp.addons.document_webdav_fast import nodes
24 | from openerp.addons.document_webdav_fast.dav_fs import dict_merge2
25 | from openerp.addons.document.document import nodefd_static
26 | from openerp.tools.safe_eval import safe_eval
27 | from datetime import datetime, timedelta, time
28 | from time import strptime, mktime, strftime
29 | from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
30 | from openerp import models, fields, api, _
31 |
32 | import logging
33 | _logger = logging.getLogger(__name__)
34 |
35 | import re
36 |
37 | try:
38 | from icalendar import Calendar, Event, vDatetime
39 | except ImportError:
40 | raise Warning('icalendar library missing, pip install icalendar')
41 |
42 | _NS_CALDAV = "urn:ietf:params:xml:ns:caldav"
43 |
44 | class node_model_calendar_collection(nodes.node_res_obj):
45 | "The children of this node are all models that implement vcalendar.model"
46 |
47 | def _get_default_node(self):
48 | return node_calendar("default", self, self.context, 'res.partner')
49 |
50 | def _get_filter_nodes(self, cr):
51 | '''find all models that implement vcalendar.model'''
52 | fields_obj = self.context._dirobj.pool.get('ir.model.fields')
53 | fields_ids = fields_obj.search(cr, self.context.uid,
54 | [('name', '=', 'vevent_uid'),
55 | ('model_id.model', '!=', 'vcalendar.model')])
56 | fields = fields_obj.browse(cr, self.context.uid, fields_ids)
57 | _logger.debug('_get_filter_nodes | %s' % fields)
58 | return [node_filter("m-%s" % _field.model_id.model, self,
59 | self.context, _field.model_id.model,
60 | _field.model_id.name)
61 | for _field in fields]
62 |
63 | def _get_filter_nodes_by_name(self, cr, ir_model=None):
64 | model_obj = self.context._dirobj.pool.get('ir.model')
65 | model_ids = model_obj.search(cr, self.context.uid,
66 | [('model', '=', ir_model)])
67 | model_data = model_obj.read(cr, self.context.uid, model_ids,
68 | ['model', 'name'])
69 | return [node_filter("m-%s" % ir_model, self,
70 | self.context, str(ir_model),
71 | _model['name'])
72 | for _model in model_data]
73 |
74 | #~ def _child_get(self, cr, name=False, parent_id=False, domain=None):
75 | #~ if name:
76 | #~ if name.startswith('m-'):
77 | #~ # _logger.error('_child_get | %s' % self._get_filter_nodes_by_name(cr, name[2:]))
78 | #~ return self._get_filter_nodes_by_name(cr, name[2:])
79 | #~ # _logger.error('_child_get | %s' % [self._get_default_node()])
80 | #~ return [self._get_default_node()]
81 | #~
82 | #~ # _logger.error('_child_get | %s | %s' % ([self._get_default_node()], self._get_filter_nodes(cr)))
83 | #~ return [self._get_default_node()] + self._get_filter_nodes(cr)
84 |
85 | def _child_get(self, cr, name=False, parent_id=False, domain=None):
86 | children = []
87 | res_partner_obj = self.context._dirobj.pool.get('res.partner')
88 | if not domain:
89 | domain = []
90 |
91 | if name:
92 | domain.append(('name', '=', name))
93 | partner_ids = res_partner_obj.search(cr, self.context.uid, domain)
94 |
95 | for partner in res_partner_obj.browse(cr, self.context.uid,
96 | partner_ids):
97 | children.append(
98 | res_node_calendar(partner.name,
99 | self, self.context, partner,
100 | None, partner.id, 'res.partner'))
101 | return children
102 |
103 | class node_filter(nodes.node_class):
104 | "The children of this node are all custom filters of a given model."
105 | DAV_M_NS = {
106 | "DAV:": '_get_dav',
107 | }
108 |
109 | def __init__(self, path, parent, context, ir_model='res.partner',
110 | displayname=''):
111 | super(node_filter, self).__init__(path, parent, context)
112 | self.mimetype = 'application/x-directory'
113 | self.create_date = parent.create_date
114 | self.ir_model = ir_model
115 | self.displayname = displayname
116 |
117 | def _get_default_node(self):
118 | return node_calendar("default", self, self.context, self.ir_model)
119 |
120 | def _get_filter_nodes(self, cr, filter_ids):
121 | filters_obj = self.context._dirobj.pool.get('ir.filters')
122 | filter_data = filters_obj.read(cr, self.context.uid, filter_ids,
123 | ['context', 'domain', 'name'])
124 | return [node_calendar("filtered-%s" % _filter['id'], self,
125 | self.context,
126 | self.ir_model, _filter['name'],
127 | _filter['domain'], _filter['id'])
128 | for _filter in filter_data]
129 |
130 | def _get_ttag(self, cr):
131 | return 'calendar-%d-%s' % (self.context.uid, self.ir_model)
132 |
133 | def get_dav_resourcetype(self, cr):
134 | return [('collection', 'DAV:')]
135 |
136 | def children(self, cr, domain=None):
137 | return self._child_get(cr, domain=domain)
138 |
139 | def child(self, cr, name, domain=None):
140 | res = self._child_get(cr, name, domain=domain)
141 | if res:
142 | return res[0]
143 | return None
144 |
145 | def _child_get(self, cr, name=False, parent_id=False, domain=None):
146 | if name:
147 | if name.startswith('filtered-'):
148 | return self._get_filter_nodes(cr, [int(name[9:])])
149 | return [self._get_default_node()]
150 |
151 | filters_obj = self.context._dirobj.pool.get('ir.filters')
152 | filter_ids = filters_obj.search(cr, self.context.uid,
153 | [('model_id', '=', self.ir_model),
154 | ('user_id', 'in', [self.context.uid, False])])
155 | return [self._get_default_node()] + \
156 | self._get_filter_nodes(cr, filter_ids)
157 |
158 | class node_calendar(nodes.node_class):
159 | """This node contains events for all records of a given model.
160 | If a filter is given, the node contains only those records
161 | that match the filter."""
162 | #~ DAV_PROPS = dict_merge2(nodes.node_dir.DAV_PROPS,
163 | #~ {"DAV:": ('supported-report-set',),
164 | #~ _NS_CALDAV: ('calendar-data',
165 | #~ 'supported-calendar-data',
166 | #~ 'max-resource-size',
167 | #~ )})
168 | #~ DAV_M_NS = {
169 | #~ "DAV:": '_get_dav',
170 | #~ _NS_CALDAV: '_get_caldav',
171 | #~ }
172 | #~ http_options = {'DAV': ['calendar-access']}
173 |
174 | our_type = 'collection'
175 | DAV_PROPS = dict_merge2(nodes.node_dir.DAV_PROPS,
176 | {"DAV:": ('supported-report-set',),
177 | _NS_CALDAV: ('calendar-description',
178 | )})
179 |
180 | DAV_PROPS_HIDDEN = {
181 | "urn:ietf:params:xml:ns:caldav": (
182 | 'calendar-data',
183 | #~ 'calendar-timezone',
184 | 'supported-calendar-data',
185 | 'max-resource-size',
186 | #~ 'min-date-time',
187 | #~ 'max-date-time',
188 | )}
189 |
190 | DAV_M_NS = {
191 | "DAV:": '_get_dav',
192 | _NS_CALDAV: '_get_caldav',
193 | }
194 | http_options = {'DAV': ['calendar-access']}
195 |
196 | def __init__(self, path, parent, context,
197 | ir_model='res.partner', filter_name=None,
198 | filter_domain=None, filter_id=None):
199 | super(node_calendar, self).__init__(path, parent, context)
200 | self.mimetype = 'application/x-directory'
201 | self.create_date = parent.create_date
202 | self.ir_model = ir_model
203 | self.filter_id = filter_id
204 | if filter_domain and self.filter_id:
205 | self.filter_domain = ['|',
206 | ('dav_filter_id', '=', self.filter_id)] + \
207 | safe_eval(filter_domain)
208 | else:
209 | self.filter_domain = []
210 | if filter_name:
211 | self.displayname = "%s filtered by %s" % (ir_model, filter_name)
212 | else:
213 | self.displayname = "%s" % path
214 |
215 | def children(self, cr, domain=None):
216 | if not domain:
217 | domain = []
218 | return self._child_get(cr, domain=(domain + self.filter_domain), name=None)
219 |
220 | def child(self, cr, name, domain=None):
221 | if not domain:
222 | domain = []
223 | res = self._child_get(cr, name, domain=(domain + self.filter_domain))
224 | if res:
225 | return res[0]
226 | return None
227 |
228 | def _child_get(self, cr, name=False, parent_id=False, domain=None):
229 | children = []
230 | res_partner_obj = self.context._dirobj.pool.get(self.ir_model)
231 | if not domain:
232 | domain = []
233 |
234 | if name:
235 | domain.append(('name', '=', name))
236 | partner_ids = res_partner_obj.search(cr, self.context.uid, domain)
237 |
238 | for partner in res_partner_obj.browse(cr, self.context.uid,
239 | partner_ids):
240 | children.append(
241 | res_node_calendar(partner.name,
242 | self, self.context, partner,
243 | None, partner.id, self.ir_model))
244 | return children
245 |
246 | def _get_ttag(self, cr):
247 | _logger.error('calendar-%d-%s' % (self.context.uid, self.path))
248 | return 'calendar-%d-%s' % (self.context.uid, self.path)
249 |
250 | def get_dav_resourcetype(self, cr):
251 | return [('collection', 'DAV:'),
252 | ('calendar', _NS_CALDAV)]
253 |
254 | def _get_dav_supported_report_set(self, cr):
255 | return ('supported-report', 'DAV:',
256 | ('report','DAV:',
257 | ('principal-match','DAV:')
258 | )
259 | )
260 |
261 | def _get_caldav_calendar_description(self, cr):
262 | return self.displayname
263 |
264 | def _get_caldav_supported_calendar_data(self, cr):
265 | return ('calendar-data', _NS_CALDAV, None,
266 | {'content-type': "text/calendar", 'version': "2.0"})
267 |
268 | def _get_caldav_max_resource_size(self, cr):
269 | return 65535
270 |
271 | # Method below is borrowed from
272 | # http://bazaar.launchpad.net/~aw/openerp-vertel/6.1/view/head:/carddav/caldav_node.py
273 | # row 221:269
274 | def get_domain(self, cr, filters):
275 | # TODO: doc.
276 | res = []
277 | if not filters:
278 | return res
279 | #~ _log = logging.getLogger('caldav.query')
280 | if filters.localName == 'calendar-query':
281 | res = []
282 | for filter_child in filters.childNodes:
283 | if filter_child.nodeType == filter_child.TEXT_NODE:
284 | continue
285 | if filter_child.localName == 'filter':
286 | for vcalendar_filter in filter_child.childNodes:
287 | if vcalendar_filter.nodeType == vcalendar_filter.TEXT_NODE:
288 | continue
289 | if vcalendar_filter.localName == 'comp-filter':
290 | if vcalendar_filter.getAttribute('name') == 'VCALENDAR':
291 | for vevent_filter in vcalendar_filter.childNodes:
292 | if vevent_filter.nodeType == vevent_filter.TEXT_NODE:
293 | continue
294 | if vevent_filter.localName == 'comp-filter':
295 | if vevent_filter.getAttribute('name'):
296 | res = [('type','=',vevent_filter.getAttribute('name').lower() )]
297 |
298 | for cfe in vevent_filter.childNodes:
299 | if cfe.localName == 'time-range':
300 | if cfe.getAttribute('start'):
301 | _logger.warning("Ignore start.. ")
302 | # No, it won't work in this API
303 | #val = cfe.getAttribute('start')
304 | #res += [('dtstart','=', cfe)]
305 | elif cfe.getAttribute('end'):
306 | _logger.warning("Ignore end.. ")
307 | else:
308 | _logger.debug("Unknown comp-filter: %s", cfe.localName)
309 | else:
310 | _logger.debug("Unknown comp-filter: %s", vevent_filter.localName)
311 | else:
312 | _logger.debug("Unknown filter element: %s", vcalendar_filter.localName)
313 | else:
314 | _logger.debug("Unknown calendar-query element: %s", filter_child.localName)
315 | return res
316 | elif filters.localName == 'calendar-multiget':
317 | # this is not the place to process, as it wouldn't support multi-level
318 | # hrefs. So, the code is moved to document_webdav/dav_fs.py
319 | pass
320 | else:
321 | _logger.debug("Unknown element in REPORT: %s", filters.localName)
322 | return res
323 |
324 | #~ def create_child(self, cr, path, data=None):
325 | #~ if not data:
326 | #~ raise ValueError("Cannot create a event with no data")
327 | #~ raise Warning('create child | %s \n| end of data.' % data)
328 | #~ res_partner_obj = self.context._dirobj.pool.get(self.ir_model)
329 | #~ uid = res_partner_obj.get_uid_by_vcard(data)
330 | #~ uid = self.pool.get('res.partner')
331 | #~ partner_id = res_partner_obj.create(cr, self.context.uid,
332 | #~ {'name': 'DUMMY_NAME',
333 | #~ 'id': uid,
334 | #~ 'event_filename': path,
335 | #~ 'dav_filter_id': self.filter_id})
336 | #~ res_partner_obj.set_vcard(cr, self.context.uid, [partner_id], data)
337 | #~ partner = res_partner_obj.browse(cr, self.context.uid, partner_id)
338 | #~ return res_node_calendar(partner.event_filename, self, self.context,
339 | #~ partner, None, None, self.ir_model)
340 |
341 | def _get_caldav_calendar_data(self, cr):
342 | if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
343 | # Never return collective data to iClients, they get confused
344 | # because they do propfind on the calendar node with Depth=1
345 | # and only expect the childrens' data
346 | return None
347 | res = []
348 | for child in self.children(cr):
349 | res.append(child._get_caldav_calendar_data(cr))
350 | return res
351 |
352 | def do_PROPFIND(self):
353 | raise Warning('Do PROPFIND')
354 |
355 | class res_node_calendar(nodes.node_class):
356 | "This node represents a single calendar"
357 | our_type = 'file'
358 | DAV_PROPS = {
359 | "urn:ietf:params:xml:ns:caldav": (
360 | 'calendar-data',
361 | )}
362 | #~ 'calendar-description',
363 |
364 | #~ DAV_PROPS_HIDDEN = {
365 | #~ "urn:ietf:params:xml:ns:caldav": (
366 | #~ 'calendar-data',
367 | #~ )}
368 |
369 | DAV_M_NS = {
370 | "urn:ietf:params:xml:ns:caldav": '_get_caldav'}
371 |
372 | http_options = {'DAV': ['calendar-access']}
373 |
374 | def __init__(self, path, parent, context, res_obj=None, res_model=None,
375 | res_id=None, ir_model=None):
376 | super(res_node_calendar, self).__init__(path, parent, context)
377 | self.mimetype = 'text/calendar'
378 | self.create_date = parent.create_date
379 | self.write_date = parent.write_date or parent.create_date
380 | self.displayname = res_obj.name or None
381 | self.ir_model = ir_model
382 | self.res_id = res_id
383 |
384 | self.res_obj = res_obj
385 | if self.res_obj:
386 | if self.res_obj.create_date:
387 | self.create_date = self.res_obj.create_date
388 | if self.res_obj.write_date:
389 | self.write_date = self.res_obj.write_date
390 |
391 | def create_child(self, cr, path, data=None):
392 | if not data:
393 | raise ValueError("Cannot create a event with no data")
394 |
395 | self.res_obj.get_caldav_partner_event(data)
396 | _logger.error('cal.event | %s\nres.partner | %s' % (self.context._dirobj.pool.get('calendar.event'), self.res_obj))
397 | if path.endswith('.ics'):
398 | path = path[:-4]
399 |
400 | return res_node_calendar(path, self, self.context, self.res_obj, None, self.res_id, self.ir_model)
401 |
402 | def open_data(self, cr, mode):
403 | return nodefd_static(self, cr, mode)
404 |
405 | def get_data(self, cr, fil_obj=None):
406 | return self.res_obj.get_caldav_calendar()
407 |
408 | #~ def get_dav_resourcetype(self, cr):
409 | #~ return ''
410 |
411 | def get_dav_resourcetype(self, cr):
412 | return [('collection', 'DAV:'),
413 | ('calendar', _NS_CALDAV)]
414 |
415 | def get_data_len(self, cr, fil_obj=None):
416 | data = self.get_data(cr, fil_obj)
417 | if data:
418 | return len(data)
419 | return 0
420 |
421 | def set_data(self, cr, data):
422 | self.res_obj.set_event(data)
423 |
424 | def _get_ttag(self, cr):
425 | return 'calendar-event-%s-%d' % (self.res_obj._name,
426 | self.res_obj.id)
427 |
428 | def rm(self, cr):
429 | uid = self.context.uid
430 | partner_obj = self.context._dirobj.pool.get(self.ir_model)
431 | return partner_obj.unlink(cr, uid, [self.res_obj.id])
432 |
433 | def _get_caldav_calendar_data(self, cr):
434 | return self.get_data(cr)
435 |
436 | class res_partner(models.Model):
437 | _inherit = "res.partner"
438 |
439 | def get_caldav_calendar(self):
440 | calendar = Calendar()
441 |
442 | exported_ics = []
443 | for event in reversed(self.env['calendar.event'].search([('partner_ids','in',self.id)])):
444 | temporary_ics = event.get_caldav_event(exported_ics, self)
445 | if temporary_ics:
446 | exported_ics.append(temporary_ics[1])
447 | calendar.add_component(temporary_ics[0])
448 |
449 | tmpCalendar = calendar.to_ical()
450 | tmpSearch = re.findall('RRULE:[^\n]*\\;[^\n]*', tmpCalendar)
451 |
452 | for counter in range(len(tmpSearch)):
453 | tmpCalendar = tmpCalendar.replace(tmpSearch[counter], tmpSearch[counter].replace('\\;', ';', tmpSearch[counter].count('\\;')))
454 |
455 | return tmpCalendar
456 |
457 | @api.multi
458 | def get_caldav_attendee_ids(self, event):
459 | partner_ids = []
460 | event_attendee_list = event.get('attendee')
461 | if event_attendee_list:
462 | if not (type(event_attendee_list) is list):
463 | event_attendee_list = [event_attendee_list]
464 |
465 | for vAttendee in event_attendee_list:
466 | attendee_mailto = re.search('(:MAILTO:)([a-zA-Z0-9_@.\-]*)', vAttendee)
467 | attendee_cn = re.search('(CN=)([^:]*)', vAttendee)
468 | if attendee_mailto:
469 | attendee_mailto = attendee_mailto.group(2)
470 | if attendee_cn:
471 | attendee_cn = attendee_cn.group(2)
472 | elif not attendee_mailto and not attendee_cn:
473 | attendee_cn = vAttendee
474 |
475 | if attendee_mailto:
476 | partner_result = self.env['res.partner'].search([('email','=',attendee_mailto)])
477 |
478 | if not partner_result:
479 | partner_id = self.env['res.partner'].create({
480 | 'email': attendee_mailto,
481 | 'name': attendee_cn or attendee_mailto,
482 | })
483 | else:
484 | partner_id = partner_result[0]
485 | elif attendee_cn:
486 | partner_result = self.env['res.partner'].search([('name','=',attendee_cn)])
487 |
488 | if not partner_result:
489 | partner_id = self.env['res.partner'].create({
490 | 'name': attendee_cn or attendee_mailto,
491 | })
492 | else:
493 | partner_id = partner_result[0]
494 |
495 | partner_ids.append(partner_id.id or None)
496 |
497 | return partner_ids
498 |
499 | def get_caldav_partner_event(self, data):
500 | return self.env['calendar.event'].set_caldav_event(data, self)
501 |
502 | class calendar_event(models.Model):
503 | _inherit = 'calendar.event'
504 |
505 | def set_caldav_event(self, ics_file, partner):
506 | for event in Calendar.from_ical(ics_file).walk('vevent'):
507 |
508 | summary = ''
509 | description = unicode(event.get('description', ''))
510 | if unicode(event.get('summary')) and len(unicode(event.get('summary'))) < 35:
511 | summary = unicode(event.get('summary'))
512 | elif len(unicode(event.get('summary'))) >= 35:
513 | summary = unicode(event.get('summary'))[:35]
514 | if not event.get('description'):
515 | description = unicode(event.get('summary'))
516 |
517 | record = {r[1]:r[2] for r in [ ('dtstart','start_date',event.get('dtstart') and event.get('dtstart').dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
518 | ('dtend','stop_date',event.get('dtend') and event.get('dtend').dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
519 | ('duration','duration',event.get('duration')),
520 | ('location','location',event.get('location') and unicode(event.get('location')) or None),
521 | #~ ('class','class',event.get('class') and str(event.get('class')) or 'private'),
522 | ('summary','name',summary),
523 | ('rrule', 'rrule',event.get('rrule') and event.get('rrule').to_ical() or None),
524 | ] if event.get(r[0])}
525 |
526 | partner_ids = self.env['res.partner'].get_caldav_attendee_ids(event)
527 | if partner_ids:
528 | partner_ids.append(partner.id)
529 | else:
530 | partner_ids = [partner.id]
531 |
532 | record['partner_ids'] = [(6,0,[partner_ids])]
533 | #~ record['ics_subscription'] = True
534 | record['start'] = record.get('start_date')
535 | record['stop'] = record.get('stop_date') or record.get('start')
536 | record['description'] = description
537 | record['show_as'] = 'busy'
538 | record['allday'] = False
539 |
540 | tmpStart = datetime.time(datetime.fromtimestamp(mktime(strptime(record['start'], DEFAULT_SERVER_DATETIME_FORMAT))))
541 | tmpStop = datetime.fromtimestamp(mktime(strptime(record['stop'], DEFAULT_SERVER_DATETIME_FORMAT)))
542 |
543 | if tmpStart == time(0,0,0) and tmpStart == datetime.time(tmpStop):
544 | record['allday'] = True
545 |
546 | if not record.get('stop_date'):
547 | record['allday'] = True
548 | record['stop_date'] = record['start_date']
549 | elif record.get('stop_date') and record['allday']:
550 | record['stop_date'] = vDatetime(tmpStop - timedelta(hours=24)).dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
551 | record['stop'] = record['stop_date']
552 | self.env['calendar.event'].create(record)
553 |
554 | @api.multi
555 | def get_caldav_event(self, events_exported, partner):
556 | """
557 | Returns iCalendar file for the event invitation.
558 | @param event: event object (browse record)
559 | @return: .ics file content
560 | """
561 | ics = Event()
562 | event = self[0]
563 |
564 | def ics_datetime(idate, allday=False):
565 | if idate:
566 | if allday:
567 | return str(vDatetime(datetime.fromtimestamp(mktime(strptime(idate, DEFAULT_SERVER_DATETIME_FORMAT)))).to_ical())[:8]
568 | else:
569 | return vDatetime(datetime.fromtimestamp(mktime(strptime(idate, DEFAULT_SERVER_DATETIME_FORMAT)))).to_ical() + 'Z'
570 | return False
571 |
572 | raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation."))
573 | ics['summary'] = event.name
574 | if event.description:
575 | ics['description'] = event.description
576 | if event.location:
577 | ics['location'] = event.location
578 | if event.rrule:
579 | ics['rrule'] = event.rrule
580 | if event.alarm_ids:
581 | for alarm in event.alarm_ids:
582 | valarm = ics.add('valarm')
583 | interval = alarm.interval
584 | duration = alarm.duration
585 | trigger = valarm.add('TRIGGER')
586 | trigger.params['related'] = ["START"]
587 | if interval == 'days':
588 | delta = timedelta(days=duration)
589 | elif interval == 'hours':
590 | delta = timedelta(hours=duration)
591 | elif interval == 'minutes':
592 | delta = timedelta(minutes=duration)
593 | trigger.value = delta
594 | valarm.add('DESCRIPTION').value = alarm.name or 'Odoo'
595 | if event.attendee_ids:
596 | for attendee in event.attendee_ids:
597 | attendee_add = ics.get('attendee')
598 | attendee_add = attendee.cn and ('CN=' + attendee.cn) or ''
599 | if attendee.cn and attendee.email:
600 | attendee_add += ':'
601 | attendee_add += attendee.email and ('MAILTO:' + attendee.email) or ''
602 |
603 | ics.add('attendee', attendee_add, encode=0)
604 |
605 | if events_exported:
606 | event_not_found = True
607 |
608 | for event_comparison in events_exported:
609 | if str(ics) == event_comparison:
610 | event_not_found = False
611 | break
612 |
613 | if event_not_found:
614 | events_exported.append(str(ics))
615 |
616 | ics['uid'] = '%s@%s-%s' % (event.id, self.env.cr.dbname, partner.id)
617 | ics['created'] = ics_datetime(strftime(DEFAULT_SERVER_DATETIME_FORMAT))
618 | tmpStart = ics_datetime(event.start, event.allday)
619 | tmpEnd = ics_datetime(event.stop, event.allday)
620 |
621 | if event.allday:
622 | ics['dtstart;value=date'] = tmpStart
623 | else:
624 | ics['dtstart'] = tmpStart
625 |
626 | if tmpStart != tmpEnd or not event.allday:
627 | if event.allday:
628 | ics['dtend;value=date'] = str(vDatetime(datetime.fromtimestamp(mktime(strptime(event.stop, DEFAULT_SERVER_DATETIME_FORMAT))) + timedelta(hours=24)).to_ical())[:8]
629 | else:
630 | ics['dtend'] = tmpEnd
631 |
632 | return [ics, events_exported]
633 |
634 | else:
635 | events_exported.append(str(ics))
636 |
637 | ics['uid'] = '%s@%s-%s' % (event.id, self.env.cr.dbname, partner.id)
638 | ics['created'] = ics_datetime(strftime(DEFAULT_SERVER_DATETIME_FORMAT))
639 | tmpStart = ics_datetime(event.start, event.allday)
640 | tmpEnd = ics_datetime(event.stop, event.allday)
641 |
642 | if event.allday:
643 | ics['dtstart;value=date'] = tmpStart
644 | else:
645 | ics['dtstart'] = tmpStart
646 |
647 | if tmpStart != tmpEnd or not event.allday:
648 | if event.allday:
649 | ics['dtend;value=date'] = str(vDatetime(datetime.fromtimestamp(mktime(strptime(event.stop, DEFAULT_SERVER_DATETIME_FORMAT))) + timedelta(hours=24)).to_ical())[:8]
650 | else:
651 | ics['dtend'] = tmpEnd
652 |
653 | return [ics, events_exported]
654 |
655 | # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4
656 |
--------------------------------------------------------------------------------