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, context=context):
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 line "%s" more than once') % (vals.get('name')))
774 return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
776 basic_calendar_line()
778 class basic_calendar_alias(osv.osv):
779 """ Mapping of client filenames to ORM ids of calendar records
781 Since some clients insist on putting arbitrary filenames on the .ics data
782 they send us, and they won't respect the redirection "Location:" header,
783 we have to store those filenames and allow clients to call our calendar
785 Note that adding a column to all tables that would possibly hold calendar-
786 mapped data won't work. The user is always allowed to specify more
787 calendars, on any arbitrary ORM object, without need to alter those tables'
790 _name = 'basic.calendar.alias'
792 'name': fields.char('Filename', size=512, required=True, select=1),
793 'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
794 select=1, help='The calendar/line this mapping applies to'),
795 'res_id': fields.integer('Res. ID', required=True, select=1),
798 _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
799 _('The same filename cannot apply to two records!')), ]
801 basic_calendar_alias()
803 class basic_calendar_attribute(osv.osv):
804 _name = 'basic.calendar.attributes'
805 _description = 'Calendar attributes'
807 'name': fields.char("Name", size=64, required=True),
808 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
809 ('alarm', 'Alarm'), \
810 ('attendee', 'Attendee')], \
811 string="Type", size=64, required=True),
814 basic_calendar_attribute()
817 class basic_calendar_fields(osv.osv):
818 """ Calendar fields """
820 _name = 'basic.calendar.fields'
821 _description = 'Calendar fields'
824 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
825 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
826 'type_id': fields.many2one('basic.calendar.lines', 'Type', \
827 required=True, ondelete='cascade'),
828 'expr': fields.char("Expression", size=64),
829 'fn': fields.selection([('field', 'Use the field'),
830 ('const', 'Expression as constant'),
831 ('hours', 'Interval in hours'),
833 'mapping': fields.text('Mapping'),
837 'fn': lambda *a: 'field',
841 ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
844 def check_line(self, cr, uid, vals, name, context=None):
845 """ check calendar's line
846 @param self: The object pointer
847 @param cr: the current row, from the database cursor,
848 @param uid: the current user’s ID for security checks,
849 @param vals: Get Values
850 @param context: A standard dictionary for contextual values
852 f_obj = self.pool.get('ir.model.fields')
853 field = f_obj.browse(cr, uid, vals['field_id'], context=context)
854 relation = field.relation
855 line_obj = self.pool.get('basic.calendar.lines')
856 l_id = line_obj.search(cr, uid, [('name', '=', name)])
858 line = line_obj.browse(cr, uid, l_id, context=context)[0]
859 line_rel = line.object_id.model
860 if (relation != 'NULL') and (not relation == line_rel):
861 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines') % (name))
864 def create(self, cr, uid, vals, context=None):
865 """ Create Calendar's fields
866 @param self: The object pointer
867 @param cr: the current row, from the database cursor,
868 @param uid: the current user’s ID for security checks,
869 @param vals: Get Values
870 @param context: A standard dictionary for contextual values
873 cr.execute('SELECT name FROM basic_calendar_attributes \
874 WHERE id=%s', (vals.get('name'),))
877 if name in ('valarm', 'attendee'):
878 self.check_line(cr, uid, vals, name, context=context)
879 return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
881 def write(self, cr, uid, ids, vals, context=None):
882 """ write Calendar's fields
883 @param self: The object pointer
884 @param cr: the current row, from the database cursor,
885 @param uid: the current user’s ID for security checks,
886 @param vals: Get Values
887 @param context: A standard dictionary for contextual values
893 field = self.browse(cr, uid, id, context=context)
894 name = field.name.name
895 if name in ('valarm', 'attendee'):
896 self.check_line(cr, uid, vals, name, context=context)
897 return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
899 basic_calendar_fields()
902 class Event(CalDAV, osv.osv_memory):
903 _name = 'basic.calendar.event'
906 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
907 '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.
908 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
909 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
910 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
911 '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.
912 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
913 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
914 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
915 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
916 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
917 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
918 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
919 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
920 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
921 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
923 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
924 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
925 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
926 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
927 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
928 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
929 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
931 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
932 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
933 '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
934 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
935 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
937 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
938 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
941 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
943 @param self: The object pointer
944 @param cr: the current row, from the database cursor,
945 @param uid: the current user’s ID for security checks,
946 @param datas: Get datas
947 @param context: A standard dictionary for contextual values
950 return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
955 class ToDo(CalDAV, osv.osv_memory):
956 _name = 'basic.calendar.todo'
994 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
996 @param self: The object pointer
997 @param cr: the current row, from the database cursor,
998 @param uid: the current user’s ID for security checks,
999 @param datas: Get datas
1000 @param context: A standard dictionary for contextual values
1003 return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1008 class Journal(CalDAV):
1013 class FreeBusy(CalDAV):
1015 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
1016 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1017 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1018 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1019 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1020 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1021 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1022 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1023 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1024 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1025 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1031 class Timezone(CalDAV, osv.osv_memory):
1032 _name = 'basic.calendar.timezone'
1033 _calname = 'vtimezone'
1036 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1037 '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.
1038 '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.
1039 'standardc': {'tzprop': None}, # Use: R-1,
1040 'daylightc': {'tzprop': None}, # Use: R-1,
1041 'x-prop': None, # Use: O-n, Type: Text,
1044 def get_name_offset(self, cr, uid, tzid, context=None):
1045 """ Get Name Offset value
1046 @param self: The object pointer
1047 @param cr: the current row, from the database cursor,
1048 @param uid: the current user’s ID for security checks,
1049 @param context: A standard dictionary for contextual values
1052 mytz = pytz.timezone(tzid.title())
1053 mydt = datetime.now(tz=mytz)
1054 offset = mydt.utcoffset()
1055 val = offset.days * 24 + float(offset.seconds) / 3600
1056 realoffset = '%02d%02d' % (math.floor(abs(val)), \
1057 round(abs(val) % 1 + 0.01, 2) * 60)
1058 realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1059 return (mydt.tzname(), realoffset)
1061 def export_cal(self, cr, uid, model, tzid, ical, context=None):
1063 @param self: The object pointer
1064 @param cr: the current row, from the database cursor,
1065 @param uid: the current user’s ID for security checks,
1066 @param model: Get Model's name
1067 @param context: A standard dictionary for contextual values
1071 ctx = context.copy()
1072 ctx.update({'model': model})
1073 cal_tz = ical.add('vtimezone')
1074 cal_tz.add('TZID').value = tzid.title()
1075 tz_std = cal_tz.add('STANDARD')
1076 tzname, offset = self.get_name_offset(cr, uid, tzid)
1077 tz_std.add("TZOFFSETFROM").value = offset
1078 tz_std.add("TZOFFSETTO").value = offset
1079 #TODO: Get start date for timezone
1080 tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1081 tz_std.add("TZNAME").value = tzname
1084 def import_cal(self, cr, uid, ical_data, context=None):
1086 @param self: The object pointer
1087 @param cr: the current row, from the database cursor,
1088 @param uid: the current user’s ID for security checks,
1089 @param ical_data: Get calendar's data
1090 @param context: A standard dictionary for contextual values
1093 for child in ical_data.getChildren():
1094 if child.name.lower() == 'tzid':
1095 tzname = child.value
1096 self.ical_set(child.name.lower(), tzname, 'value')
1097 vals = map_data(cr, uid, self, context=context)
1103 class Alarm(CalDAV, osv.osv_memory):
1104 _name = 'basic.calendar.alarm'
1108 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1109 '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
1110 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
1111 '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
1112 '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
1113 '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
1114 '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
1115 '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.
1119 def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1121 @param self: The object pointer
1122 @param cr: the current row, from the database cursor,
1123 @param uid: the current user’s ID for security checks,
1124 @param model: Get Model's name
1125 @param alarm_id: Get Alarm's Id
1126 @param context: A standard dictionary for contextual values
1130 valarm = vevent.add('valarm')
1131 alarm_object = self.pool.get(model)
1132 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1134 # Compute trigger data
1135 interval = alarm_data['trigger_interval']
1136 occurs = alarm_data['trigger_occurs']
1137 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1138 or -(alarm_data['trigger_duration'])
1139 related = alarm_data['trigger_related']
1140 trigger = valarm.add('TRIGGER')
1141 trigger.params['related'] = [related.upper()]
1142 if interval == 'days':
1143 delta = timedelta(days=duration)
1144 if interval == 'hours':
1145 delta = timedelta(hours=duration)
1146 if interval == 'minutes':
1147 delta = timedelta(minutes=duration)
1148 trigger.value = delta
1150 # Compute other details
1151 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1152 valarm.add('ACTION').value = alarm_data['action']
1155 def import_cal(self, cr, uid, ical_data, context=None):
1157 @param self: The object pointer
1158 @param cr: the current row, from the database cursor,
1159 @param uid: the current user’s ID for security checks,
1160 @param ical_data: Get calendar's Data
1161 @param context: A standard dictionary for contextual values
1164 ctx = context.copy()
1165 ctx.update({'model': context.get('model', None)})
1166 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1167 for child in ical_data.getChildren():
1168 if child.name.lower() == 'trigger':
1169 seconds = child.value.seconds
1170 days = child.value.days
1171 diff = (days * 86400) + seconds
1175 duration = abs(days)
1176 related = days > 0 and 'after' or 'before'
1177 elif (abs(diff) / 3600) == 0:
1178 duration = abs(diff / 60)
1179 interval = 'minutes'
1180 related = days >= 0 and 'after' or 'before'
1182 duration = abs(diff / 3600)
1184 related = days >= 0 and 'after' or 'before'
1185 self.ical_set('trigger_interval', interval, 'value')
1186 self.ical_set('trigger_duration', duration, 'value')
1187 self.ical_set('trigger_occurs', related.lower(), 'value')
1189 if child.params.get('related'):
1190 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1192 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1193 vals = map_data(cr, uid, self, context=context)
1199 class Attendee(CalDAV, osv.osv_memory):
1200 _name = 'basic.calendar.attendee'
1201 _calname = 'attendee'
1204 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1205 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
1206 '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"
1207 '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".
1208 '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.
1209 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1210 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1211 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1212 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
1213 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
1214 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
1217 def import_cal(self, cr, uid, ical_data, context=None):
1219 @param self: The object pointer
1220 @param cr: the current row, from the database cursor,
1221 @param uid: the current user’s ID for security checks,
1222 @param ical_data: Get calendar's Data
1223 @param context: A standard dictionary for contextual values
1226 ctx = context.copy()
1227 ctx.update({'model': context.get('model', None)})
1228 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1229 for para in ical_data.params:
1230 if para.lower() == 'cn':
1231 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1232 ical_data.value, 'value')
1234 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1235 if not ical_data.params.get('CN'):
1236 self.ical_set('cn', ical_data.value, 'value')
1237 vals = map_data(cr, uid, self, context=context)
1240 def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1242 @param self: The object pointer
1243 @param cr: the current row, from the database cursor,
1244 @param uid: the current user’s ID for security checks,
1245 @param model: Get model's name
1246 @param attendee_ids: Get Attendee's Id
1247 @param context: A standard dictionary for contextual values
1251 attendee_object = self.pool.get(model)
1252 ctx = context.copy()
1253 ctx.update({'model': model})
1254 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1255 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1256 attendee_add = vevent.add('attendee')
1258 for a_key, a_val in self.__attribute__.items():
1259 if attendee[a_val['field']] and a_val['field'] != 'cn':
1260 if a_val['type'] in ('text', 'char', 'selection'):
1261 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1262 elif a_val['type'] == 'boolean':
1263 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1264 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1265 cn_val = [str(attendee[a_val['field']])]
1267 attendee_add.params['CN'] = cn_val
1268 if not attendee['email']:
1269 attendee_add.value = 'MAILTO:'
1270 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1271 elif attendee['email']:
1272 attendee_add.value = 'MAILTO:' + attendee['email']
1277 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: