1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil import parser
24 from dateutil.rrule import *
25 from osv import osv, fields
26 from tools.translate import _
34 from caldav_node import res_node_calendar
35 from orm_utils import get_last_modified
36 from tools.safe_eval import safe_eval as eval
41 raise osv.except_osv('vobject Import Error!','Please install python-vobject \
42 from http://vobject.skyhouseconsulting.com/')
44 # O-1 Optional and can come only once
45 # O-n Optional and can come more than once
46 # R-1 Required and can come only once
47 # R-n Required and can come more than once
49 def uid2openobjectid(cr, uidval, oomodel, rdate):
50 """ UID To Open Object Id
51 @param cr: the current row, from the database cursor,
52 @param uidval: Get USerId vale
53 @oomodel: Open Object ModelName
54 @param rdate: Get Recurrent Date
56 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
59 wematch = __rege.match(uidval.encode('utf8'))
63 model, id, dbname = wematch.groups()
64 model_obj = pooler.get_pool(cr.dbname).get(model)
65 if (not model == oomodel) or (not dbname == cr.dbname):
67 qry = 'SELECT DISTINCT(id) FROM %s' % model_obj._table
69 qry += " WHERE recurrent_id=%s"
70 cr.execute(qry, (rdate,))
77 ids = map(lambda x: str(x[0]), cr.fetchall())
82 def openobjectid2uid(cr, uidval, oomodel):
83 """ Gives the value of UID for VEVENT
84 @param cr: the current row, from the database cursor,
85 @param uidval: Id value of the Event
86 @oomodel: Open Object ModelName """
88 value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
92 """Take a dict of mail and convert to string.
95 if isinstance(arg, dict):
101 rstr = ard.get('name','')
102 if ard.get('company',False):
103 rstr += ' (%s)' % ard.get('company')
105 rstr += ' <%s>' % ard.get('email')
107 return ', '.join(ret)
109 def str2mailto(emailstr, multi=False):
110 """Split one email string to a dict of name, company, mail parts
112 @param multi Return an array, recognize comma-sep
114 # TODO: move to tools or sth.
115 mege = re.compile(r'([^\(\<]+) *(\((.*?)\))? *(\< ?(.*?) ?\>)? ?(\((.*?)\))? *$')
120 mailz = emailstr.split(',')
123 m = mege.match(mas.strip())
125 # one of the rare non-matching strings is "sad" :(
126 # retz.append({ 'name': mas.strip() })
128 raise ValueError("Invalid email address %r" % mas)
129 rd = { 'name': m.group(1).strip(),
130 'email': m.group(5), }
132 rd['company'] = m.group(3).strip()
134 rd['company'] = m.group(7).strip()
136 if rd['name'].startswith('"') and rd['name'].endswith('"'):
137 rd['name'] = rd['name'][1:-1]
145 def get_attribute_mapping(cr, uid, calname, context=None):
146 """ Attribute Mapping with Basic calendar fields and lines
147 @param cr: the current row, from the database cursor,
148 @param uid: the current user’s ID for security checks,
149 @param calname: Get Calendar name
150 @param context: A standard dictionary for contextual values """
154 pool = pooler.get_pool(cr.dbname)
155 field_obj = pool.get('basic.calendar.fields')
156 type_obj = pool.get('basic.calendar.lines')
157 domain = [('object_id.model', '=', context.get('model'))]
158 if context.get('calendar_id'):
159 domain.append(('calendar_id', '=', context.get('calendar_id')))
160 type_id = type_obj.search(cr, uid, domain)
161 fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])])
163 for field in field_obj.browse(cr, uid, fids):
164 attr = field.name.name
166 res[attr]['field'] = field.field_id.name
167 res[attr]['type'] = field.field_id.ttype
168 if field.fn == 'hours':
169 res[attr]['type'] = "timedelta"
170 if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
171 res[attr]['object'] = field.field_id.relation
172 elif res[attr]['type'] in ('selection') and field.mapping:
173 res[attr]['mapping'] = eval(field.mapping)
174 if not res.get('uid', None):
176 res['uid']['field'] = 'id'
177 res['uid']['type'] = "integer"
180 def map_data(cr, uid, obj, context=None):
182 @param self: The object pointer
183 @param cr: the current row, from the database cursor,"""
186 for map_dict in obj.__attribute__:
187 map_val = obj.ical_get(map_dict, 'value')
188 field = obj.ical_get(map_dict, 'field')
189 field_type = obj.ical_get(map_dict, 'type')
191 if field_type == 'selection':
194 if type(map_val) == list and len(map_val): #TOFIX: why need to check this
196 mapping = obj.__attribute__[map_dict].get('mapping', False)
198 map_val = mapping.get(map_val.lower(), False)
200 map_val = map_val.lower()
201 if field_type == 'many2many':
206 model = obj.__attribute__[map_dict].get('object', False)
207 modobj = obj.pool.get(model)
208 for map_vall in map_val:
209 id = modobj.create(cr, uid, map_vall, context=context)
211 vals[field] = [(6, 0, ids)]
213 if field_type == 'many2one':
215 if not map_val or not isinstance(map_val, dict):
218 model = obj.__attribute__[map_dict].get('object', False)
219 modobj = obj.pool.get(model)
220 # check if the record exists or not
221 key1 = map_val.keys()
222 value1 = map_val.values()
223 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
224 exist_id = modobj.search(cr, uid, domain, context=context)
228 id = modobj.create(cr, uid, map_val, context=context)
231 if field_type == 'timedelta':
233 vals[field] = (map_val.seconds/float(86400) + map_val.days)
234 vals[field] = map_val
237 class CalDAV(object):
239 _logger = logging.getLogger('document.caldav')
241 def ical_set(self, name, value, type):
242 """ set calendar Attribute
243 @param self: The object pointer,
244 @param name: Get Attribute Name
245 @param value: Get Attribute Value
246 @param type: Get Attribute Type
248 if name in self.__attribute__ and self.__attribute__[name]:
249 self.__attribute__[name][type] = value
252 def ical_get(self, name, type):
253 """ Get calendar Attribute
254 @param self: The object pointer,
255 @param name: Get Attribute Name
256 @param type: Get Attribute Type
259 if self.__attribute__.get(name):
260 val = self.__attribute__.get(name).get(type, None)
261 valtype = self.__attribute__.get(name).get('type', None)
263 if valtype and valtype == 'datetime' and val:
264 if isinstance(val, list):
265 val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
267 val = val.strftime('%Y-%m-%d %H:%M:%S')
270 return self.__attribute__.get(name, None)
272 def ical_reset(self, type):
273 """ Reset Calendar Attribute
274 @param self: The object pointer,
275 @param type: Get Attribute Type
278 for name in self.__attribute__:
279 if self.__attribute__[name]:
280 self.__attribute__[name][type] = None
283 def format_date_tz(self, src_date, tz=None):
284 """ This function converts date into specifice timezone value
285 @param src_date: Date to be converted (datetime.datetime)
286 @return: Converted datetime.datetime object for the date
288 format = tools.DEFAULT_SERVER_DATETIME_FORMAT
289 date_str = src_date.strftime('%Y-%m-%d %H:%M:%S')
290 res_date = tools.server_to_local_timestamp(date_str, format, format, tz)
291 return datetime.strptime(res_date, "%Y-%m-%d %H:%M:%S")
293 def parse_ics(self, cr, uid, child, cal_children=None, context=None):
294 """ parse calendaring and scheduling information
295 @param self: The object pointer
296 @param cr: the current row, from the database cursor,
297 @param uid: the current user’s ID for security checks,
298 @param context: A standard dictionary for contextual values """
302 _server_tzinfo = pytz.timezone(tools.get_server_timezone())
304 for cal_data in child.getChildren():
305 if cal_data.name.lower() == 'organizer':
306 dmail = { 'name': cal_data.params.get('CN', ['',])[0],
307 'email': cal_data.value.lower().replace('mailto:',''),
310 self.ical_set(cal_data.name.lower(), mailto2str(dmail), 'value')
312 if cal_data.name.lower() == 'attendee':
315 ctx.update({'model': cal_children[cal_data.name.lower()]})
316 attendee = self.pool.get('basic.calendar.attendee')
317 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
318 self.ical_set(cal_data.name.lower(), att_data, 'value')
320 if cal_data.name.lower() == 'valarm':
321 alarm = self.pool.get('basic.calendar.alarm')
324 ctx.update({'model': cal_children[cal_data.name.lower()]})
325 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
326 self.ical_set(cal_data.name.lower(), vals, 'value')
328 if cal_data.name.lower() == 'exdate':
329 exdates += cal_data.value
331 for exdate in exdates:
332 exvals.append(datetime.fromtimestamp(time.mktime(exdate.utctimetuple())).strftime('%Y%m%dT%H%M%S'))
333 self.ical_set(cal_data.name.lower(), ','.join(exvals), 'value')
335 if cal_data.name.lower() in self.__attribute__:
336 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
337 self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
338 date_local = cal_data.value.astimezone(_server_tzinfo)
339 self.ical_set(cal_data.name.lower(), date_local, 'value')
341 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
342 vals = map_data(cr, uid, self, context=context)
345 def create_ics(self, cr, uid, datas, name, ical, context=None):
346 """ create calendaring and scheduling information
347 @param self: The object pointer
348 @param cr: the current row, from the database cursor,
349 @param uid: the current user’s ID for security checks,
350 @param context: A standard dictionary for contextual values """
359 vevent = ical.add(name)
360 for field in self.__attribute__.keys():
361 map_field = self.ical_get(field, 'field')
362 map_type = self.ical_get(field, 'type')
363 if map_field in data.keys():
365 model = context.get('model', None)
368 uidval = openobjectid2uid(cr, data[map_field], model)
369 #Computation for getting events with the same UID (RFC4791 Section4.1)
371 model_obj = self.pool.get(model)
373 if model_obj._columns.get('recurrent_uid', None):
374 cr.execute('SELECT id FROM %s WHERE recurrent_uid=%%s' % model_obj._table,
376 r_ids = map(lambda x: x[0], cr.fetchall())
378 r_datas = model_obj.read(cr, uid, r_ids, context=context)
379 rcal = CalDAV.export_cal(self, cr, uid, r_datas, 'vevent', context=context)
380 for revents in rcal.contents.get('vevent', []):
381 ical.contents['vevent'].append(revents)
383 if data.get('recurrent_uid', None):
384 # Change the UID value in case of modified event from any recurrent event
385 uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
386 vevent.add('uid').value = uidval
387 elif field == 'attendee' and data[map_field]:
388 model = self.__attribute__[field].get('object', False)
389 attendee_obj = self.pool.get('basic.calendar.attendee')
390 vevent = attendee_obj.export_cal(cr, uid, model, \
391 data[map_field], vevent, context=context)
392 elif field == 'valarm' and data[map_field]:
393 model = self.__attribute__[field].get('object', False)
395 ctx.update({'model': model})
396 alarm_obj = self.pool.get('basic.calendar.alarm')
397 vevent = alarm_obj.export_cal(cr, uid, model, \
398 data[map_field][0], vevent, context=ctx)
399 elif field == 'vtimezone' and data[map_field]:
400 tzval = data[map_field]
401 if tzval not in timezones:
402 tz_obj = self.pool.get('basic.calendar.timezone')
403 ical = tz_obj.export_cal(cr, uid, None, \
404 data[map_field], ical, context=context)
405 timezones.append(data[map_field])
406 if vevent.contents.get('recurrence-id'):
407 # Convert recurrence-id field value accroding to timezone value
408 recurid_val = vevent.contents.get('recurrence-id')[0].value
409 vevent.contents.get('recurrence-id')[0].params['TZID'] = [tzval.title()]
410 vevent.contents.get('recurrence-id')[0].value = self.format_date_tz(recurid_val, tzval.title())
412 # Set exdates according to timezone value
413 # This is the case when timezone mapping comes after the exdate mapping
414 # and we have exdate value available
415 exfield.params['TZID'] = [tzval.title()]
417 for exdate in exdates:
418 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
419 exfield.value = exdates_updated
420 elif field == 'organizer' and data[map_field]:
421 organizer = str2mailto(data[map_field])
422 event_org = vevent.add('organizer')
423 event_org.params['CN'] = [organizer['name']]
424 event_org.value = 'MAILTO:' + (organizer.get('email') or '')
426 elif data[map_field]:
427 if map_type in ("char", "text"):
428 if field in ('exdate'):
429 exfield = vevent.add(field)
430 exdates = (data[map_field]).split(',')
432 # Set exdates according to timezone value
433 # This is the case when timezone mapping comes before the exdate mapping
434 # and we have timezone value available
435 exfield.params['TZID'] = [tzval.title()]
437 for exdate in exdates:
438 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
439 exfield.value = exdates_updated
441 vevent.add(field).value = tools.ustr(data[map_field])
442 elif map_type in ('datetime', 'date') and data[map_field]:
443 dtfield = vevent.add(field)
445 # Export the date according to the event timezone value
446 dtfield.params['TZID'] = [tzval.title()]
447 dtfield.value = self.format_date_tz(parser.parse(data[map_field]), tzval.title())
449 dtfield.value = parser.parse(data[map_field])
450 elif map_type == "timedelta":
451 vevent.add(field).value = timedelta(hours=data[map_field])
452 elif map_type == "many2one":
453 vevent.add(field).value = tools.ustr(data.get(map_field)[1])
454 elif map_type in ("float", "integer"):
455 vevent.add(field).value = str(data.get(map_field))
456 elif map_type == "selection":
457 if not self.ical_get(field, 'mapping'):
458 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
460 for key1, val1 in self.ical_get(field, 'mapping').items():
461 if val1 == data[map_field]:
462 vevent.add(field).value = key1.upper()
465 def check_import(self, cr, uid, vals, context=None):
467 @param self: The object pointer
468 @param cr: the current row, from the database cursor,
469 @param uid: the current user’s ID for security checks,
470 @param vals: Get Values
471 @param context: A standard dictionary for contextual values
476 model_obj = self.pool.get(context.get('model'))
480 # Compute value of duration
481 if 'date_deadline' in val and 'duration' not in val:
482 start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
483 end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
485 val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
486 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
487 val.get('recurrent_id'))
488 if val.has_key('create_date'):
489 val.pop('create_date')
490 u_id = val.get('id', None)
493 val.update({'recurrent_uid': exists})
494 model_obj.write(cr, uid, [r_id], val)
497 model_obj.write(cr, uid, [exists], val)
500 if u_id in recur_pool and val.get('recurrent_id'):
501 val.update({'recurrent_uid': recur_pool[u_id]})
502 revent_id = model_obj.create(cr, uid, val)
503 ids.append(revent_id)
505 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
506 wematch = __rege.match(u_id.encode('utf8'))
508 model, recur_id, dbname = wematch.groups()
509 val.update({'recurrent_uid': recur_id})
510 event_id = model_obj.create(cr, uid, val)
511 recur_pool[u_id] = event_id
517 def export_cal(self, cr, uid, datas, vobj=None, context=None):
519 @param self: The object pointer
520 @param cr: the current row, from the database cursor,
521 @param uid: the current user’s ID for security checks,
522 @param datas: Get Data's for caldav
523 @param context: A standard dictionary for contextual values
526 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
527 ical = vobject.iCalendar()
528 self.create_ics(cr, uid, datas, vobj, ical, context=context)
531 raise # osv.except_osv(('Error !'), (str(e)))
533 def import_cal(self, cr, uid, content, data_id=None, context=None):
535 @param self: The object pointer
536 @param cr: the current row, from the database cursor,
537 @param uid: the current user’s ID for security checks,
538 @param data_id: Get Data’s ID or False
539 @param context: A standard dictionary for contextual values
543 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
544 parsedCal = vobject.readOne(ical_data)
547 for child in parsedCal.getChildren():
548 if child.name.lower() in ('vevent', 'vtodo'):
549 vals = self.parse_ics(cr, uid, child, context=context)
553 if vals: res.append(vals)
554 self.ical_reset('value')
557 class Calendar(CalDAV, osv.osv):
558 _name = 'basic.calendar'
559 _calname = 'calendar'
562 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
563 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
564 # or the minimum and maximum range of the iCalendar specification
565 # that is required in order to interpret the iCalendar object.
566 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
567 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
568 'vevent': None, # Use: O-n, Type: Collection of Event class
569 'vtodo': None, # Use: O-n, Type: Collection of ToDo class
570 'vjournal': None, # Use: O-n, Type: Collection of Journal class
571 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
572 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
575 'name': fields.char("Name", size=64),
576 'user_id': fields.many2one('res.users', 'Owner'),
577 'collection_id': fields.many2one('document.directory', 'Collection', \
579 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
580 string="Type", size=64),
581 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
582 'create_date': fields.datetime('Created Date', readonly=True),
583 'write_date': fields.datetime('Modifided Date', readonly=True),
584 'description': fields.text("description"),
585 'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
586 'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
587 'has_webcal': fields.boolean('WebCal', required=True, help="Also export a <name>.ics entry next to the calendar folder, with WebCal content."),
594 def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
600 ctx_res_id = context.get('res_id', None)
601 ctx_model = context.get('model', None)
602 for cal in self.browse(cr, uid, ids):
603 for line in cal.line_ids:
604 if ctx_model and ctx_model != line.object_id.model:
606 if line.name in ('valarm', 'attendee'):
608 line_domain = eval(line.domain or '[]', context)
609 line_domain += domain
611 line_domain += [('id','=',ctx_res_id)]
612 mod_obj = self.pool.get(line.object_id.model)
613 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
614 for data in mod_obj.browse(cr, uid, data_ids, context):
615 ctx = parent and parent.context or None
616 if hasattr(data, 'recurrent_uid') and data.recurrent_uid:
617 # Skip for event which is child of other event
619 node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
624 def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
630 ctx_res_id = context.get('res_id', None)
631 ctx_model = context.get('model', None)
632 for cal in self.browse(cr, uid, ids):
633 for line in cal.line_ids:
634 if ctx_model and ctx_model != line.object_id.model:
636 if line.name in ('valarm', 'attendee'):
638 line_domain = eval(line.domain or '[]', context)
639 line_domain += domain
641 line_domain += [('id','=',ctx_res_id)]
642 mod_obj = self.pool.get(line.object_id.model)
643 max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
644 if res and res > max_data:
649 def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
651 @param ids: List of calendar’s IDs
652 @param vobj: the type of object to export
653 @return the ical data.
657 ctx_model = context.get('model', None)
658 ctx_res_id = context.get('res_id', None)
659 ical = vobject.iCalendar()
660 for cal in self.browse(cr, uid, ids):
661 for line in cal.line_ids:
662 if ctx_model and ctx_model != line.object_id.model:
664 if line.name in ('valarm', 'attendee'):
666 domain = eval(line.domain or '[]', context)
668 domain += [('id','=',ctx_res_id)]
669 mod_obj = self.pool.get(line.object_id.model)
670 data_ids = mod_obj.search(cr, uid, domain, context=context)
671 datas = mod_obj.read(cr, uid, data_ids, context=context)
672 context.update({'model': line.object_id.model,
673 'calendar_id': cal.id
675 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
676 self.create_ics(cr, uid, datas, line.name, ical, context=context)
677 return ical.serialize()
679 def import_cal(self, cr, uid, content, data_id=None, context=None):
681 @param self: The object pointer
682 @param cr: the current row, from the database cursor,
683 @param uid: the current user’s ID for security checks,
684 @param data_id: Get Data’s ID or False
685 @param context: A standard dictionary for contextual values
691 parsedCal = vobject.readOne(ical_data)
693 data_id = self.search(cr, uid, [])[0]
694 cal = self.browse(cr, uid, data_id, context=context)
697 for line in cal.line_ids:
698 cal_children[line.name] = line.object_id.model
701 for child in parsedCal.getChildren():
702 if child.name.lower() in cal_children:
703 context.update({'model': cal_children[child.name.lower()],
704 'calendar_id': cal['id']
706 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
707 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
709 objs.append(cal_children[child.name.lower()])
710 elif child.name.upper() == 'CALSCALE':
711 if child.value.upper() != 'GREGORIAN':
712 self._logger.warning('How do I handle %s calendars?',child.value)
713 elif child.name.upper() in ('PRODID', 'VERSION'):
715 elif child.name.upper().startswith('X-'):
716 self._logger.debug("skipping custom node %s", child.name)
718 self._logger.debug("skipping node %s", child.name)
721 for obj_name in list(set(objs)):
722 obj = self.pool.get(obj_name)
723 if hasattr(obj, 'check_import'):
724 r = obj.check_import(cr, uid, vals, context=context)
729 r = self.check_import(cr, uid, vals, context=context)
736 class basic_calendar_line(osv.osv):
737 """ Calendar Lines """
739 _name = 'basic.calendar.lines'
740 _description = 'Calendar Lines'
743 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
744 ('valarm', 'Alarm'), \
745 ('attendee', 'Attendee')], \
746 string="Type", size=64),
747 'object_id': fields.many2one('ir.model', 'Object'),
748 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
749 required=True, ondelete='cascade'),
750 'domain': fields.char('Domain', size=124),
751 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
755 'domain': lambda *a: '[]',
758 def create(self, cr, uid, vals, context=None):
759 """ create calendar's line
760 @param self: The object pointer
761 @param cr: the current row, from the database cursor,
762 @param uid: the current user’s ID for security checks,
763 @param vals: Get the Values
764 @param context: A standard dictionary for contextual values
767 cr.execute("SELECT COUNT(id) FROM basic_calendar_lines \
768 WHERE name=%s AND calendar_id=%s",
769 (vals.get('name'), vals.get('calendar_id')))
773 raise osv.except_osv(_('Warning !'), _('Can not create \
774 line "%s" more than once' % (vals.get('name'))))
775 return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
777 basic_calendar_line()
779 class basic_calendar_alias(osv.osv):
780 """ Mapping of client filenames to ORM ids of calendar records
782 Since some clients insist on putting arbitrary filenames on the .ics data
783 they send us, and they won't respect the redirection "Location:" header,
784 we have to store those filenames and allow clients to call our calendar
786 Note that adding a column to all tables that would possibly hold calendar-
787 mapped data won't work. The user is always allowed to specify more
788 calendars, on any arbitrary ORM object, without need to alter those tables'
791 _name = 'basic.calendar.alias'
793 'name': fields.char('Filename', size=512, required=True, select=1),
794 'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
795 select=1, help='The calendar/line this mapping applies to'),
796 'res_id': fields.integer('Res. ID', required=True, select=1),
799 _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
800 _('The same filename cannot apply to two records!')), ]
802 basic_calendar_alias()
804 class basic_calendar_attribute(osv.osv):
805 _name = 'basic.calendar.attributes'
806 _description = 'Calendar attributes'
808 'name': fields.char("Name", size=64, required=True),
809 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
810 ('alarm', 'Alarm'), \
811 ('attendee', 'Attendee')], \
812 string="Type", size=64, required=True),
815 basic_calendar_attribute()
818 class basic_calendar_fields(osv.osv):
819 """ Calendar fields """
821 _name = 'basic.calendar.fields'
822 _description = 'Calendar fields'
825 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
826 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
827 'type_id': fields.many2one('basic.calendar.lines', 'Type', \
828 required=True, ondelete='cascade'),
829 'expr': fields.char("Expression", size=64),
830 'fn': fields.selection([('field', 'Use the field'),
831 ('const', 'Expression as constant'),
832 ('hours', 'Interval in hours'),
834 'mapping': fields.text('Mapping'),
838 'fn': lambda *a: 'field',
842 ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
845 def check_line(self, cr, uid, vals, name, context=None):
846 """ check calendar's line
847 @param self: The object pointer
848 @param cr: the current row, from the database cursor,
849 @param uid: the current user’s ID for security checks,
850 @param vals: Get Values
851 @param context: A standard dictionary for contextual values
853 f_obj = self.pool.get('ir.model.fields')
854 field = f_obj.browse(cr, uid, vals['field_id'], context=context)
855 relation = field.relation
856 line_obj = self.pool.get('basic.calendar.lines')
857 l_id = line_obj.search(cr, uid, [('name', '=', name)])
859 line = line_obj.browse(cr, uid, l_id, context=context)[0]
860 line_rel = line.object_id.model
861 if (relation != 'NULL') and (not relation == line_rel):
862 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name)))
865 def create(self, cr, uid, vals, context=None):
866 """ Create Calendar's fields
867 @param self: The object pointer
868 @param cr: the current row, from the database cursor,
869 @param uid: the current user’s ID for security checks,
870 @param vals: Get Values
871 @param context: A standard dictionary for contextual values
874 cr.execute('SELECT name FROM basic_calendar_attributes \
875 WHERE id=%s', (vals.get('name'),))
878 if name in ('valarm', 'attendee'):
879 self.check_line(cr, uid, vals, name, context=context)
880 return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
882 def write(self, cr, uid, ids, vals, context=None):
883 """ write Calendar's fields
884 @param self: The object pointer
885 @param cr: the current row, from the database cursor,
886 @param uid: the current user’s ID for security checks,
887 @param vals: Get Values
888 @param context: A standard dictionary for contextual values
894 field = self.browse(cr, uid, id, context=context)
895 name = field.name.name
896 if name in ('valarm', 'attendee'):
897 self.check_line(cr, uid, vals, name, context=context)
898 return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
900 basic_calendar_fields()
903 class Event(CalDAV, osv.osv_memory):
904 _name = 'basic.calendar.event'
907 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
908 'created': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the calendar information was created by the calendar user agent in the calendar store.
909 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
910 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
911 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
912 'last-mod': None, # Use: O-1, Type: DATE-TIME Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
913 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
914 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
915 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
916 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
917 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
918 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
919 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
920 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
921 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
922 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
924 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
925 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
926 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
927 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
928 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
929 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
930 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
932 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
933 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
934 'resources': None, # Use: O-n, Type: TEXT, Defines the equipment or resources anticipated for an activity specified by a calendar entity like RESOURCES:EASEL,PROJECTOR,VCR, LANGUAGE=fr:1 raton-laveur
935 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
936 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
938 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
939 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
942 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
944 @param self: The object pointer
945 @param cr: the current row, from the database cursor,
946 @param uid: the current user’s ID for security checks,
947 @param datas: Get datas
948 @param context: A standard dictionary for contextual values
951 return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
956 class ToDo(CalDAV, osv.osv_memory):
957 _name = 'basic.calendar.todo'
995 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
997 @param self: The object pointer
998 @param cr: the current row, from the database cursor,
999 @param uid: the current user’s ID for security checks,
1000 @param datas: Get datas
1001 @param context: A standard dictionary for contextual values
1004 return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1009 class Journal(CalDAV):
1014 class FreeBusy(CalDAV):
1016 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
1017 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1018 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1019 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1020 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1021 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1022 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1023 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1024 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1025 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1026 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1032 class Timezone(CalDAV, osv.osv_memory):
1033 _name = 'basic.calendar.timezone'
1034 _calname = 'vtimezone'
1037 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1038 'last-mod': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
1039 'tzurl': None, # Use: O-1, Type: URI, Provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to-date version of itself.
1040 'standardc': {'tzprop': None}, # Use: R-1,
1041 'daylightc': {'tzprop': None}, # Use: R-1,
1042 'x-prop': None, # Use: O-n, Type: Text,
1045 def get_name_offset(self, cr, uid, tzid, context=None):
1046 """ Get Name Offset value
1047 @param self: The object pointer
1048 @param cr: the current row, from the database cursor,
1049 @param uid: the current user’s ID for security checks,
1050 @param context: A standard dictionary for contextual values
1053 mytz = pytz.timezone(tzid.title())
1054 mydt = datetime.now(tz=mytz)
1055 offset = mydt.utcoffset()
1056 val = offset.days * 24 + float(offset.seconds) / 3600
1057 realoffset = '%02d%02d' % (math.floor(abs(val)), \
1058 round(abs(val) % 1 + 0.01, 2) * 60)
1059 realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1060 return (mydt.tzname(), realoffset)
1062 def export_cal(self, cr, uid, model, tzid, ical, context=None):
1064 @param self: The object pointer
1065 @param cr: the current row, from the database cursor,
1066 @param uid: the current user’s ID for security checks,
1067 @param model: Get Model's name
1068 @param context: A standard dictionary for contextual values
1072 ctx = context.copy()
1073 ctx.update({'model': model})
1074 cal_tz = ical.add('vtimezone')
1075 cal_tz.add('TZID').value = tzid.title()
1076 tz_std = cal_tz.add('STANDARD')
1077 tzname, offset = self.get_name_offset(cr, uid, tzid)
1078 tz_std.add("TZOFFSETFROM").value = offset
1079 tz_std.add("TZOFFSETTO").value = offset
1080 #TODO: Get start date for timezone
1081 tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1082 tz_std.add("TZNAME").value = tzname
1085 def import_cal(self, cr, uid, ical_data, context=None):
1087 @param self: The object pointer
1088 @param cr: the current row, from the database cursor,
1089 @param uid: the current user’s ID for security checks,
1090 @param ical_data: Get calendar's data
1091 @param context: A standard dictionary for contextual values
1094 for child in ical_data.getChildren():
1095 if child.name.lower() == 'tzid':
1096 tzname = child.value
1097 self.ical_set(child.name.lower(), tzname, 'value')
1098 vals = map_data(cr, uid, self, context=context)
1104 class Alarm(CalDAV, osv.osv_memory):
1105 _name = 'basic.calendar.alarm'
1109 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1110 'description': None, # Type: Text, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. Use:- R-1 for DISPLAY,Use:- R-1 for EMAIL,Use:- R-1 for PROCEDURE
1111 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
1112 'attendee': None, # Use: R-n, Type: CAL-ADDRESS, Contain the email address of attendees to receive the message. It can also include one or more. Use for EMAIL
1113 'trigger': None, # Use: R-1, Type: DURATION, The "TRIGGER" property specifies a duration prior to the start of an event or a to-do. The "TRIGGER" edge may be explicitly set to be relative to the "START" or "END" of the event or to-do with the "related" parameter of the "TRIGGER" property. The "TRIGGER" property value type can alternatively be set to an absolute calendar date and time of day value. Use for all action like AUDIO, DISPLAY, EMAIL and PROCEDURE
1114 'duration': None, # Type: DURATION, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
1115 'repeat': None, # Type: INTEGER, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
1116 'attach': None, # Use:- O-n: which MUST point to a sound resource, which is rendered when the alarm is triggered for AUDIO, Use:- O-n: which are intended to be sent as message attachments for EMAIL, Use:- R-1:which MUST point to a procedure resource, which is invoked when the alarm is triggered for PROCEDURE.
1120 def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1122 @param self: The object pointer
1123 @param cr: the current row, from the database cursor,
1124 @param uid: the current user’s ID for security checks,
1125 @param model: Get Model's name
1126 @param alarm_id: Get Alarm's Id
1127 @param context: A standard dictionary for contextual values
1131 valarm = vevent.add('valarm')
1132 alarm_object = self.pool.get(model)
1133 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1135 # Compute trigger data
1136 interval = alarm_data['trigger_interval']
1137 occurs = alarm_data['trigger_occurs']
1138 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1139 or -(alarm_data['trigger_duration'])
1140 related = alarm_data['trigger_related']
1141 trigger = valarm.add('TRIGGER')
1142 trigger.params['related'] = [related.upper()]
1143 if interval == 'days':
1144 delta = timedelta(days=duration)
1145 if interval == 'hours':
1146 delta = timedelta(hours=duration)
1147 if interval == 'minutes':
1148 delta = timedelta(minutes=duration)
1149 trigger.value = delta
1151 # Compute other details
1152 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1153 valarm.add('ACTION').value = alarm_data['action']
1156 def import_cal(self, cr, uid, ical_data, context=None):
1158 @param self: The object pointer
1159 @param cr: the current row, from the database cursor,
1160 @param uid: the current user’s ID for security checks,
1161 @param ical_data: Get calendar's Data
1162 @param context: A standard dictionary for contextual values
1165 ctx = context.copy()
1166 ctx.update({'model': context.get('model', None)})
1167 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1168 for child in ical_data.getChildren():
1169 if child.name.lower() == 'trigger':
1170 seconds = child.value.seconds
1171 days = child.value.days
1172 diff = (days * 86400) + seconds
1176 duration = abs(days)
1177 related = days > 0 and 'after' or 'before'
1178 elif (abs(diff) / 3600) == 0:
1179 duration = abs(diff / 60)
1180 interval = 'minutes'
1181 related = days >= 0 and 'after' or 'before'
1183 duration = abs(diff / 3600)
1185 related = days >= 0 and 'after' or 'before'
1186 self.ical_set('trigger_interval', interval, 'value')
1187 self.ical_set('trigger_duration', duration, 'value')
1188 self.ical_set('trigger_occurs', related.lower(), 'value')
1190 if child.params.get('related'):
1191 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1193 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1194 vals = map_data(cr, uid, self, context=context)
1200 class Attendee(CalDAV, osv.osv_memory):
1201 _name = 'basic.calendar.attendee'
1202 _calname = 'attendee'
1205 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1206 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
1207 'role': None, # Use: 0-1 Specify the participation role for the calendar user specified by the property like "CHAIR"/"REQ-PARTICIPANT"/"OPT-PARTICIPANT"/"NON-PARTICIPANT"
1208 'partstat': None, # Use: 0-1 Specify the participation status for the calendar user specified by the property. like use for VEVENT:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED", use for VTODO:-"NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED"/"COMPLETED"/"IN-PROCESS" and use for VJOURNAL:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED".
1209 'rsvp': None, # Use: 0-1 Specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value like TRUE / FALSE.
1210 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1211 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1212 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1213 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
1214 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
1215 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
1218 def import_cal(self, cr, uid, ical_data, context=None):
1220 @param self: The object pointer
1221 @param cr: the current row, from the database cursor,
1222 @param uid: the current user’s ID for security checks,
1223 @param ical_data: Get calendar's Data
1224 @param context: A standard dictionary for contextual values
1227 ctx = context.copy()
1228 ctx.update({'model': context.get('model', None)})
1229 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1230 for para in ical_data.params:
1231 if para.lower() == 'cn':
1232 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1233 ical_data.value, 'value')
1235 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1236 if not ical_data.params.get('CN'):
1237 self.ical_set('cn', ical_data.value, 'value')
1238 vals = map_data(cr, uid, self, context=context)
1241 def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1243 @param self: The object pointer
1244 @param cr: the current row, from the database cursor,
1245 @param uid: the current user’s ID for security checks,
1246 @param model: Get model's name
1247 @param attendee_ids: Get Attendee's Id
1248 @param context: A standard dictionary for contextual values
1252 attendee_object = self.pool.get(model)
1253 ctx = context.copy()
1254 ctx.update({'model': model})
1255 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1256 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1257 attendee_add = vevent.add('attendee')
1259 for a_key, a_val in self.__attribute__.items():
1260 if attendee[a_val['field']] and a_val['field'] != 'cn':
1261 if a_val['type'] in ('text', 'char', 'selection'):
1262 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1263 elif a_val['type'] == 'boolean':
1264 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1265 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1266 cn_val = [str(attendee[a_val['field']])]
1268 attendee_add.params['CN'] = cn_val
1269 if not attendee['email']:
1270 attendee_add.value = 'MAILTO:'
1271 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1272 elif attendee['email']:
1273 attendee_add.value = 'MAILTO:' + attendee['email']
1278 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: