├── 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 | --------------------------------------------------------------------------------