[ADD]: Images: caldav, crm_caldav, document_webdav, project_caldav
[odoo/odoo.git] / addons / caldav / calendar.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from 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 _
27 import math
28 import pooler
29 import pytz
30 import re
31 import tools
32 import time
33 import logging
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
37
38 try:
39     import vobject
40 except ImportError:
41     raise osv.except_osv(_('vobject Import Error!'), _('Please install python-vobject from http://vobject.skyhouseconsulting.com/'))
42
43 # O-1  Optional and can come only once
44 # O-n  Optional and can come more than once
45 # R-1  Required and can come only once
46 # R-n  Required and can come more than once
47
48 def uid2openobjectid(cr, uidval, oomodel, rdate):
49     """ UID To Open Object Id
50         @param cr: the current row, from the database cursor,
51         @param uidval: Get USerId vale
52         @oomodel: Open Object ModelName
53         @param rdate: Get Recurrent Date
54     """
55     __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
56     if not uidval:
57         return (False, None)
58     wematch = __rege.match(uidval.encode('utf8'))
59     if not wematch:
60         return (False, None)
61     else:
62         model, id, dbname = wematch.groups()
63         model_obj = pooler.get_pool(cr.dbname).get(model)
64         if (not model == oomodel) or (not dbname == cr.dbname):
65             return (False, None)
66         qry = 'SELECT DISTINCT(id) FROM %s' % model_obj._table
67         if rdate:
68             qry += " WHERE recurrent_id=%s"
69             cr.execute(qry, (rdate,))
70             r_id = cr.fetchone()
71             if r_id:
72                 return (id, r_id[0])
73             else:
74                 return (False, None)
75         cr.execute(qry)
76         ids = map(lambda x: str(x[0]), cr.fetchall())
77         if id in ids:
78             return (id, None)
79         return (False, None)
80
81 def openobjectid2uid(cr, uidval, oomodel):
82     """ Gives the value of UID for VEVENT
83         @param cr: the current row, from the database cursor,
84         @param uidval: Id value of the Event
85         @oomodel: Open Object ModelName """
86
87     value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
88     return value
89
90 def mailto2str(arg):
91     """Take a dict of mail and convert to string.
92     """
93     ret = []
94     if isinstance(arg, dict):
95         args = [arg,]
96     else:
97         args = arg
98
99     for ard in args:
100         rstr = ard.get('name','')
101         if ard.get('company',False):
102             rstr += ' (%s)' % ard.get('company')
103         if ard.get('email'):
104             rstr += ' <%s>' % ard.get('email')
105         ret.append(rstr)
106     return ', '.join(ret)
107
108 def str2mailto(emailstr, multi=False):
109     """Split one email string to a dict of name, company, mail parts
110
111        @param multi Return an array, recognize comma-sep
112     """
113     # TODO: move to tools or sth.
114     mege = re.compile(r'([^\(\<]+) *(\((.*?)\))? *(\< ?(.*?) ?\>)? ?(\((.*?)\))? *$')
115
116     mailz= [emailstr,]
117     retz = []
118     if multi:
119         mailz = emailstr.split(',')
120
121     for mas in mailz:
122         m = mege.match(mas.strip())
123         if not m:
124             # one of the rare non-matching strings is "sad" :(
125             # retz.append({ 'name': mas.strip() })
126             # continue
127             raise ValueError("Invalid email address %r" % mas)
128         rd = {  'name': m.group(1).strip(),
129                 'email': m.group(5), }
130         if m.group(2):
131             rd['company'] = m.group(3).strip()
132         elif m.group(6):
133             rd['company'] = m.group(7).strip()
134
135         if rd['name'].startswith('"') and rd['name'].endswith('"'):
136             rd['name'] = rd['name'][1:-1]
137         retz.append(rd)
138
139     if multi:
140         return retz
141     else:
142         return retz[0]
143
144 def get_attribute_mapping(cr, uid, calname, context=None):
145     """ Attribute Mapping with Basic calendar fields and lines
146         @param cr: the current row, from the database cursor,
147         @param uid: the current user’s ID for security checks,
148         @param calname: Get Calendar name
149         @param context: A standard dictionary for contextual values """
150
151     if context is None:
152         context = {}
153     pool = pooler.get_pool(cr.dbname)
154     field_obj = pool.get('basic.calendar.fields')
155     type_obj = pool.get('basic.calendar.lines')
156     domain = [('object_id.model', '=', context.get('model'))]
157     if context.get('calendar_id'):
158         domain.append(('calendar_id', '=', context.get('calendar_id')))
159     type_id = type_obj.search(cr, uid, domain)
160     fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])])
161     res = {}
162     for field in field_obj.browse(cr, uid, fids, context=context):
163         attr = field.name.name
164         res[attr] = {}
165         res[attr]['field'] = field.field_id.name
166         res[attr]['type'] = field.field_id.ttype
167         if field.fn == 'datetime_utc':
168             res[attr]['type'] = 'utc'
169         if field.fn == 'hours':
170             res[attr]['type'] = "timedelta"
171         if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
172             res[attr]['object'] = field.field_id.relation
173         elif res[attr]['type'] in ('selection') and field.mapping:
174             res[attr]['mapping'] = eval(field.mapping)
175     if not res.get('uid', None):
176         res['uid'] = {}
177         res['uid']['field'] = 'id'
178         res['uid']['type'] = "integer"
179     return res
180
181 def map_data(cr, uid, obj, context=None):
182     """ Map Data
183         @param self: The object pointer
184         @param cr: the current row, from the database cursor,"""
185
186     vals = {}
187     for map_dict in obj.__attribute__:
188         map_val = obj.ical_get(map_dict, 'value')
189         field = obj.ical_get(map_dict, 'field')
190         field_type = obj.ical_get(map_dict, 'type')
191         if field:
192             if field_type == 'selection':
193                 if not map_val:
194                     continue
195                 if type(map_val) == list and len(map_val): #TOFIX: why need to check this
196                     map_val = map_val[0]
197                 mapping = obj.__attribute__[map_dict].get('mapping', False)
198                 if mapping:
199                     map_val = mapping.get(map_val.lower(), False)
200                 else:
201                     map_val = map_val.lower()
202             if field_type == 'many2many':
203                 ids = []
204                 if not map_val:
205                     vals[field] = ids
206                     continue
207                 model = obj.__attribute__[map_dict].get('object', False)
208                 modobj = obj.pool.get(model)
209                 for map_vall in map_val:
210                     id = modobj.create(cr, uid, map_vall, context=context)
211                     ids.append(id)
212                 vals[field] = [(6, 0, ids)]
213                 continue
214             if field_type == 'many2one':
215                 id = None
216                 if not map_val or not isinstance(map_val, dict):
217                     vals[field] = id
218                     continue
219                 model = obj.__attribute__[map_dict].get('object', False)
220                 modobj = obj.pool.get(model)
221                 # check if the record exists or not
222                 key1 = map_val.keys()
223                 value1 = map_val.values()
224                 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
225                 exist_id = modobj.search(cr, uid, domain, context=context)
226                 if exist_id:
227                     id = exist_id[0]
228                 else:
229                     id = modobj.create(cr, uid, map_val, context=context)
230                 vals[field] = id
231                 continue
232             if field_type == 'timedelta':
233                 if map_val:
234                     vals[field] = (map_val.seconds/float(86400) + map_val.days)
235             vals[field] = map_val
236     return vals
237
238 class CalDAV(object):
239     __attribute__ = {}
240     _logger = logging.getLogger('document.caldav')
241
242     def ical_set(self, name, value, type):
243         """ set calendar Attribute
244          @param self: The object pointer,
245          @param name: Get Attribute Name
246          @param value: Get Attribute Value
247          @param type: Get Attribute Type
248         """
249         if name in self.__attribute__ and self.__attribute__[name]:
250             self.__attribute__[name][type] = value
251         return True
252
253     def ical_get(self, name, type):
254         """ Get calendar Attribute
255          @param self: The object pointer,
256          @param name: Get Attribute Name
257          @param type: Get Attribute Type
258         """
259         if self.__attribute__.get(name):
260             val = self.__attribute__.get(name).get(type, None)
261             valtype =  self.__attribute__.get(name).get('type', None)
262             if type == 'value':
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))
266                     else:
267                         val = val.strftime('%Y-%m-%d %H:%M:%S')
268             return  val
269         else:
270             return  self.__attribute__.get(name, None)
271
272     def ical_reset(self, type):
273         """ Reset Calendar Attribute
274          @param self: The object pointer,
275          @param type: Get Attribute Type
276         """
277         for name in self.__attribute__:
278             if self.__attribute__[name]:
279                 self.__attribute__[name][type] = None
280         return True
281
282     def format_date_tz(self, src_date, tz=None):
283         """ This function converts date into specifice timezone value
284         @param src_date: Date to be converted (datetime.datetime)
285         @return: Converted datetime.datetime object for the date
286         """
287         format = tools.DEFAULT_SERVER_DATETIME_FORMAT
288         date_str = src_date.strftime('%Y-%m-%d %H:%M:%S')
289         res_date = tools.server_to_local_timestamp(date_str, format, format, tz)
290         return datetime.strptime(res_date, "%Y-%m-%d %H:%M:%S")
291
292     def parse_ics(self, cr, uid, child, cal_children=None, context=None):
293         """ parse calendaring and scheduling information
294         @param self: The object pointer
295         @param cr: the current row, from the database cursor,
296         @param uid: the current user’s ID for security checks,
297         @param context: A standard dictionary for contextual values """
298
299         att_data = []
300         exdates = []
301         _server_tzinfo = pytz.timezone(tools.get_server_timezone())
302
303         for cal_data in child.getChildren():
304             if cal_data.name.lower() == 'organizer':
305                 dmail = { 'name': cal_data.params.get('CN', ['',])[0],
306                             'email': cal_data.value.lower().replace('mailto:',''),
307                             # TODO: company? 
308                 }
309                 self.ical_set(cal_data.name.lower(), mailto2str(dmail), 'value')
310                 continue
311             if cal_data.name.lower() == 'attendee':
312                 ctx = context.copy()
313                 if cal_children:
314                     ctx.update({'model': cal_children[cal_data.name.lower()]})
315                 attendee = self.pool.get('basic.calendar.attendee')
316                 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
317                 self.ical_set(cal_data.name.lower(), att_data, 'value')
318                 continue
319             if cal_data.name.lower() == 'valarm':
320                 alarm = self.pool.get('basic.calendar.alarm')
321                 ctx = context.copy()
322                 if cal_children:
323                     ctx.update({'model': cal_children[cal_data.name.lower()]})
324                 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
325                 self.ical_set(cal_data.name.lower(), vals, 'value')
326                 continue
327             if cal_data.name.lower() == 'exdate':
328                 exdates += cal_data.value
329                 exvals = []
330                 for exdate in exdates:
331                     exvals.append(datetime.fromtimestamp(time.mktime(exdate.utctimetuple())).strftime('%Y%m%dT%H%M%S'))
332                 self.ical_set(cal_data.name.lower(), ','.join(exvals), 'value')
333                 continue
334             if cal_data.name.lower() in self.__attribute__:
335                 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
336                     self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
337                     date_local = cal_data.value.astimezone(_server_tzinfo)
338                     self.ical_set(cal_data.name.lower(), date_local, 'value')
339                     continue
340                 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
341         vals = map_data(cr, uid, self, context=context)
342         return vals
343
344     def create_ics(self, cr, uid, datas, name, ical, context=None):
345         """ create calendaring and scheduling information
346         @param self: The object pointer
347         @param cr: the current row, from the database cursor,
348         @param uid: the current user’s ID for security checks,
349         @param context: A standard dictionary for contextual values """
350
351         if not datas:
352             return
353         timezones = []
354         for data in datas:
355             tzval = None
356             exfield = None
357             exdates = []
358             vevent = ical.add(name)
359             for field in self.__attribute__.keys():
360                 map_field = self.ical_get(field, 'field')
361                 map_type = self.ical_get(field, 'type')
362                 if map_field in data.keys():
363                     if field == 'uid':
364                         model = context.get('model', None)
365                         if not model:
366                             continue
367                         uidval = openobjectid2uid(cr, data[map_field], model)
368                         #Computation for getting events with the same UID (RFC4791 Section4.1)
369                         #START
370                         model_obj = self.pool.get(model)
371                         r_ids = []
372                         if model_obj._columns.get('recurrent_uid', None):
373                             cr.execute('SELECT id FROM %s WHERE recurrent_uid=%%s' % model_obj._table,
374                                         (data[map_field],))
375                             r_ids = map(lambda x: x[0], cr.fetchall())
376                         if r_ids:
377                             r_datas = model_obj.read(cr, uid, r_ids, context=context)
378                             rcal = CalDAV.export_cal(self, cr, uid, r_datas, 'vevent', context=context)
379                             for revents in rcal.contents.get('vevent', []):
380                                 ical.contents['vevent'].append(revents)
381                         #END
382                         if data.get('recurrent_uid', None):
383                             # Change the UID value in case of modified event from any recurrent event 
384                             uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
385                         vevent.add('uid').value = uidval
386                     elif field == 'attendee' and data[map_field]:
387                         model = self.__attribute__[field].get('object', False)
388                         attendee_obj = self.pool.get('basic.calendar.attendee')
389                         vevent = attendee_obj.export_cal(cr, uid, model, \
390                                      data[map_field], vevent, context=context)
391                     elif field == 'valarm' and data[map_field]:
392                         model = self.__attribute__[field].get('object', False)
393                         ctx = context.copy()
394                         ctx.update({'model': model})
395                         alarm_obj = self.pool.get('basic.calendar.alarm')
396                         vevent = alarm_obj.export_cal(cr, uid, model, \
397                                     data[map_field][0], vevent, context=ctx)
398                     elif field == 'vtimezone' and data[map_field]:
399                         tzval = data[map_field]
400                         if tzval not in timezones:
401                             tz_obj = self.pool.get('basic.calendar.timezone')
402                             ical = tz_obj.export_cal(cr, uid, None, \
403                                          data[map_field], ical, context=context)
404                             timezones.append(data[map_field])
405                         if vevent.contents.get('recurrence-id'):
406                             # Convert recurrence-id field value accroding to timezone value
407                             recurid_val = vevent.contents.get('recurrence-id')[0].value
408                             vevent.contents.get('recurrence-id')[0].params['TZID'] = [tzval.title()]
409                             vevent.contents.get('recurrence-id')[0].value =  self.format_date_tz(recurid_val, tzval.title())
410                         if exfield:
411                             # Set exdates according to timezone value
412                             # This is the case when timezone mapping comes after the exdate mapping
413                             # and we have exdate value available 
414                             exfield.params['TZID'] = [tzval.title()]
415                             exdates_updated = []
416                             for exdate in exdates:
417                                 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
418                             exfield.value = exdates_updated
419                     elif field == 'organizer' and data[map_field]:
420                         organizer = str2mailto(data[map_field])
421                         event_org = vevent.add('organizer')
422                         event_org.params['CN'] = [organizer['name']]
423                         event_org.value = 'MAILTO:' + (organizer.get('email') or '')
424                         # TODO: company?
425                     elif data[map_field]:
426                         if map_type in ("char", "text"):
427                             if field in ('exdate'):
428                                 exfield = vevent.add(field)
429                                 exdates = (data[map_field]).split(',')
430                                 if tzval:
431                                     # Set exdates according to timezone value
432                                     # This is the case when timezone mapping comes before the exdate mapping
433                                     # and we have timezone value available 
434                                     exfield.params['TZID'] = [tzval.title()]
435                                     exdates_updated = []
436                                     for exdate in exdates:
437                                         exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
438                                     exfield.value = exdates_updated
439                             else:
440                                 vevent.add(field).value = tools.ustr(data[map_field])
441                         elif map_type in ('datetime', 'date') and data[map_field]:
442                             dtfield = vevent.add(field)
443                             if tzval:
444                                 # Export the date according to the event timezone value
445                                 dtfield.params['TZID'] = [tzval.title()]
446                                 dtfield.value = self.format_date_tz(parser.parse(data[map_field]), tzval.title())
447                             else:
448                                 dtfield.value = parser.parse(data[map_field])
449                                 
450                         elif map_type == 'utc'and data[map_field]:
451                             if tzval:
452                                 local = pytz.timezone (tzval.title())
453                                 naive = datetime.strptime (data[map_field], "%Y-%m-%d %H:%M:%S")
454                                 local_dt = naive.replace (tzinfo = local)
455                                 utc_dt = local_dt.astimezone (pytz.utc)
456                                 vevent.add(field).value = utc_dt
457                             else:
458                                utc_timezone = pytz.timezone ('UTC')
459                                naive = datetime.strptime (data[map_field], "%Y-%m-%d %H:%M:%S")
460                                local_dt = naive.replace (tzinfo = utc_timezone)
461                                utc_dt = local_dt.astimezone (pytz.utc)
462                                vevent.add(field).value = utc_dt
463
464                         elif map_type == "timedelta":
465                             vevent.add(field).value = timedelta(hours=data[map_field])
466                         elif map_type == "many2one":
467                             vevent.add(field).value = tools.ustr(data.get(map_field)[1])
468                         elif map_type in ("float", "integer"):
469                             vevent.add(field).value = str(data.get(map_field))
470                         elif map_type == "selection":
471                             if not self.ical_get(field, 'mapping'):
472                                 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
473                             else:
474                                 for key1, val1 in self.ical_get(field, 'mapping').items():
475                                     if val1 == data[map_field]:
476                                         vevent.add(field).value = key1.upper()
477         return vevent
478
479     def check_import(self, cr, uid, vals, context=None):
480         """
481             @param self: The object pointer
482             @param cr: the current row, from the database cursor,
483             @param uid: the current user’s ID for security checks,
484             @param vals: Get Values
485             @param context: A standard dictionary for contextual values
486         """
487         if context is None:
488             context = {}
489         ids = []
490         model_obj = self.pool.get(context.get('model'))
491         recur_pool = {}
492         try:
493             for val in vals:
494                 # Compute value of duration
495                 if 'date_deadline' in val and 'duration' not in val:
496                     start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
497                     end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
498                     diff = end - start
499                     val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
500                 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
501                                                                  val.get('recurrent_id'))
502                 if val.has_key('create_date'):
503                     val.pop('create_date')
504                 u_id = val.get('id', None)
505                 val.pop('id')
506                 if exists and r_id:
507                     val.update({'recurrent_uid': exists})
508                     model_obj.write(cr, uid, [r_id], val)
509                     ids.append(r_id)
510                 elif exists:
511                     model_obj.write(cr, uid, [exists], val)
512                     ids.append(exists)
513                 else:
514                     if u_id in recur_pool and val.get('recurrent_id'):
515                         val.update({'recurrent_uid': recur_pool[u_id]})
516                         revent_id = model_obj.create(cr, uid, val)
517                         ids.append(revent_id)
518                     else:
519                         __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
520                         wematch = __rege.match(u_id.encode('utf8'))
521                         if wematch:
522                             model, recur_id, dbname = wematch.groups()
523                             val.update({'recurrent_uid': recur_id})
524                         event_id = model_obj.create(cr, uid, val)
525                         recur_pool[u_id] = event_id
526                         ids.append(event_id)
527         except Exception:
528             raise
529         return ids
530
531     def export_cal(self, cr, uid, datas, vobj=None, context=None):
532         """ Export Calendar
533             @param self: The object pointer
534             @param cr: the current row, from the database cursor,
535             @param uid: the current user’s ID for security checks,
536             @param datas: Get Data's for caldav
537             @param context: A standard dictionary for contextual values
538         """
539         try:
540             self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
541             ical = vobject.iCalendar()
542             self.create_ics(cr, uid, datas, vobj, ical, context=context)
543             return ical
544         except:
545             raise  # osv.except_osv(('Error !'), (str(e)))
546
547     def import_cal(self, cr, uid, content, data_id=None, context=None):
548         """ Import Calendar
549             @param self: The object pointer
550             @param cr: the current row, from the database cursor,
551             @param uid: the current user’s ID for security checks,
552             @param data_id: Get Data’s ID or False
553             @param context: A standard dictionary for contextual values
554         """
555
556         ical_data = content
557         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
558         parsedCal = vobject.readOne(ical_data)
559         res = []
560         vals = {}
561         for child in parsedCal.getChildren():
562             if child.name.lower() in ('vevent', 'vtodo'):
563                 vals = self.parse_ics(cr, uid, child, context=context)
564             else:
565                 vals = {}
566                 continue
567             if vals: res.append(vals)
568             self.ical_reset('value')
569         return res
570
571 class Calendar(CalDAV, osv.osv):
572     _name = 'basic.calendar'
573     _calname = 'calendar'
574
575     __attribute__ = {
576         'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
577         'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
578                            #             or the minimum and maximum range of the iCalendar specification
579                            #             that is required in order to interpret the iCalendar object.
580         'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
581         'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
582         'vevent': None, # Use: O-n, Type: Collection of Event class
583         'vtodo': None, # Use: O-n, Type: Collection of ToDo class
584         'vjournal': None, # Use: O-n, Type: Collection of Journal class
585         'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
586         'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
587     }
588     _columns = {
589             'name': fields.char("Name", size=64),
590             'user_id': fields.many2one('res.users', 'Owner'),
591             'collection_id': fields.many2one('document.directory', 'Collection', \
592                                            required=True),
593             'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
594                                     string="Type", size=64),
595             'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
596             'create_date': fields.datetime('Created Date', readonly=True),
597             'write_date': fields.datetime('Write Date', readonly=True),
598             'description': fields.text("Description"),
599             'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
600             'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
601             'has_webcal': fields.boolean('WebCal', required=True, help="Also export a <name>.ics entry next to the calendar folder, with WebCal content."),
602     }
603     
604     _defaults = {
605         'has_webcal': False,
606     }
607
608     def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
609         if context is None:
610             context = {}
611         if not domain:
612             domain = []
613         res = []
614         ctx_res_id = context.get('res_id', None)
615         ctx_model = context.get('model', None)
616         for cal in self.browse(cr, uid, ids):
617             for line in cal.line_ids:
618                 if ctx_model and ctx_model != line.object_id.model:
619                     continue
620                 if line.name in ('valarm', 'attendee'):
621                     continue
622                 line_domain = eval(line.domain or '[]', context)
623                 line_domain += domain
624                 if ctx_res_id:
625                     line_domain += [('id','=',ctx_res_id)]
626                 mod_obj = self.pool.get(line.object_id.model)
627                 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
628                 for data in mod_obj.browse(cr, uid, data_ids, context):
629                     ctx = parent and parent.context or None
630                     if hasattr(data, 'recurrent_uid') and data.recurrent_uid:
631                         # Skip for event which is child of other event
632                         continue
633                     node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
634                     res.append(node)
635         return res
636         
637
638     def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
639         if context is None:
640             context = {}
641         if not domain:
642             domain = []
643         res = None
644         ctx_res_id = context.get('res_id', None)
645         ctx_model = context.get('model', None)
646         for cal in self.browse(cr, uid, ids):
647             for line in cal.line_ids:
648                 if ctx_model and ctx_model != line.object_id.model:
649                     continue
650                 if line.name in ('valarm', 'attendee'):
651                     continue
652                 line_domain = eval(line.domain or '[]', context)
653                 line_domain += domain
654                 if ctx_res_id:
655                     line_domain += [('id','=',ctx_res_id)]
656                 mod_obj = self.pool.get(line.object_id.model)
657                 max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
658                 if res and res > max_data:
659                     continue
660                 res = max_data
661         return res
662
663     def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
664         """ Export Calendar
665             @param ids: List of calendar’s IDs
666             @param vobj: the type of object to export
667             @return the ical data.
668         """
669         if context is None:
670            context = {}
671         ctx_model = context.get('model', None)
672         ctx_res_id = context.get('res_id', None)
673         ical = vobject.iCalendar()
674         for cal in self.browse(cr, uid, ids, context=context):
675             for line in cal.line_ids:
676                 if ctx_model and ctx_model != line.object_id.model:
677                     continue
678                 if line.name in ('valarm', 'attendee'):
679                     continue
680                 domain = eval(line.domain or '[]', context)
681                 if ctx_res_id:
682                     domain += [('id','=',ctx_res_id)]
683                 mod_obj = self.pool.get(line.object_id.model)
684                 data_ids = mod_obj.search(cr, uid, domain, context=context)
685                 datas = mod_obj.read(cr, uid, data_ids, context=context)
686                 context.update({'model': line.object_id.model,
687                                         'calendar_id': cal.id
688                                         })
689                 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
690                 self.create_ics(cr, uid, datas, line.name, ical, context=context)
691         return ical.serialize()
692
693     def import_cal(self, cr, uid, content, data_id=None, context=None):
694         """ Import Calendar
695             @param self: The object pointer
696             @param cr: the current row, from the database cursor,
697             @param uid: the current user’s ID for security checks,
698             @param data_id: Get Data’s ID or False
699             @param context: A standard dictionary for contextual values
700         """
701         if context is None:
702             context = {}
703         vals = []
704         ical_data = content
705         parsedCal = vobject.readOne(ical_data)
706         if not data_id:
707             data_id = self.search(cr, uid, [])[0]
708         cal = self.browse(cr, uid, data_id, context=context)
709         cal_children = {}
710
711         for line in cal.line_ids:
712             cal_children[line.name] = line.object_id.model
713         objs = []
714         checked = True
715         for child in parsedCal.getChildren():
716             if child.name.lower() in cal_children:
717                 context.update({'model': cal_children[child.name.lower()],
718                                 'calendar_id': cal['id']
719                                 })
720                 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
721                 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
722                 vals.append(val)
723                 objs.append(cal_children[child.name.lower()])
724             elif child.name.upper() == 'CALSCALE':
725                 if child.value.upper() != 'GREGORIAN':
726                     self._logger.warning('How do I handle %s calendars?',child.value)
727             elif child.name.upper() in ('PRODID', 'VERSION'):
728                 pass
729             elif child.name.upper().startswith('X-'):
730                 self._logger.debug("skipping custom node %s", child.name)
731             else:
732                 self._logger.debug("skipping node %s", child.name)
733         
734         res = []
735         for obj_name in list(set(objs)):
736             obj = self.pool.get(obj_name)
737             if hasattr(obj, 'check_import'):
738                 r = obj.check_import(cr, uid, vals, context=context)
739                 checked = True
740                 res.extend(r)
741
742         if not checked:
743             r = self.check_import(cr, uid, vals, context=context)
744             res.extend(r)
745         return res
746
747 Calendar()
748
749
750 class basic_calendar_line(osv.osv):
751     """ Calendar Lines """
752
753     _name = 'basic.calendar.lines'
754     _description = 'Calendar Lines'
755
756     _columns = {
757             'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
758                                     ('valarm', 'Alarm'), \
759                                     ('attendee', 'Attendee')], \
760                                     string="Type", size=64),
761             'object_id': fields.many2one('ir.model', 'Object'),
762             'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
763                                        required=True, ondelete='cascade'),
764             'domain': fields.char('Domain', size=124),
765             'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
766     }
767
768     _defaults = {
769         'domain': lambda *a: '[]',
770     }
771
772     def create(self, cr, uid, vals, context=None):
773         """ create calendar's line
774             @param self: The object pointer
775             @param cr: the current row, from the database cursor,
776             @param uid: the current user’s ID for security checks,
777             @param vals: Get the Values
778             @param context: A standard dictionary for contextual values
779         """
780
781         cr.execute("SELECT COUNT(id) FROM basic_calendar_lines \
782                                 WHERE name=%s AND calendar_id=%s", 
783                                 (vals.get('name'), vals.get('calendar_id')))
784         res = cr.fetchone()
785         if res:
786             if res[0] > 0:
787                 raise osv.except_osv(_('Warning !'), _('Can not create line "%s" more than once') % (vals.get('name')))
788         return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
789
790 basic_calendar_line()
791
792 class basic_calendar_alias(osv.osv):
793     """ Mapping of client filenames to ORM ids of calendar records
794     
795         Since some clients insist on putting arbitrary filenames on the .ics data
796         they send us, and they won't respect the redirection "Location:" header, 
797         we have to store those filenames and allow clients to call our calendar
798         records with them.
799         Note that adding a column to all tables that would possibly hold calendar-
800         mapped data won't work. The user is always allowed to specify more 
801         calendars, on any arbitrary ORM object, without need to alter those tables'
802         data or structure
803     """
804     _name = 'basic.calendar.alias'
805     _columns = {
806         'name': fields.char('Filename', size=512, required=True, select=1),
807         'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
808                         select=1, help='The calendar/line this mapping applies to'),
809         'res_id': fields.integer('Res. ID', required=True, select=1),
810         }
811         
812     _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
813                 _('The same filename cannot apply to two records!')), ]
814
815 basic_calendar_alias()
816
817 class basic_calendar_attribute(osv.osv):
818     _name = 'basic.calendar.attributes'
819     _description = 'Calendar attributes'
820     _columns = {
821         'name': fields.char("Name", size=64, required=True),
822         'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
823                                     ('alarm', 'Alarm'), \
824                                     ('attendee', 'Attendee')], \
825                                     string="Type", size=64, required=True),
826     }
827
828 basic_calendar_attribute()
829
830
831 class basic_calendar_fields(osv.osv):
832     """ Calendar fields """
833
834     _name = 'basic.calendar.fields'
835     _description = 'Calendar fields'
836     _order = 'name'
837
838     _columns = {
839         'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
840         'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
841         'type_id': fields.many2one('basic.calendar.lines', 'Type', \
842                                    required=True, ondelete='cascade'),
843         'expr': fields.char("Expression", size=64),
844         'fn': fields.selection([('field', 'Use the field'),
845                         ('const', 'Expression as constant'),
846                         ('hours', 'Interval in hours'),
847                         ('datetime_utc', 'Datetime In UTC'),
848                         ], 'Function'),
849         'mapping': fields.text('Mapping'),
850     }
851
852     _defaults = {
853         'fn': 'field',
854     }
855
856     _sql_constraints = [
857         ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
858     ]
859
860     def check_line(self, cr, uid, vals, name, context=None):
861         """ check calendar's line
862             @param self: The object pointer
863             @param cr: the current row, from the database cursor,
864             @param uid: the current user’s ID for security checks,
865             @param vals: Get Values
866             @param context: A standard dictionary for contextual values
867         """
868         f_obj = self.pool.get('ir.model.fields')
869         field = f_obj.browse(cr, uid, vals['field_id'], context=context)
870         relation = field.relation
871         line_obj = self.pool.get('basic.calendar.lines')
872         l_id = line_obj.search(cr, uid, [('name', '=', name)])
873         if l_id:
874             line = line_obj.browse(cr, uid, l_id, context=context)[0]
875             line_rel = line.object_id.model
876             if (relation != 'NULL') and (not relation == line_rel):
877                 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines') % (name))
878         return True
879
880     def create(self, cr, uid, vals, context=None):
881         """ Create Calendar's fields
882             @param self: The object pointer
883             @param cr: the current row, from the database cursor,
884             @param uid: the current user’s ID for security checks,
885             @param vals: Get Values
886             @param context: A standard dictionary for contextual values
887         """
888
889         cr.execute('SELECT name FROM basic_calendar_attributes \
890                             WHERE id=%s', (vals.get('name'),))
891         name = cr.fetchone()
892         name = name[0]
893         if name in ('valarm', 'attendee'):
894             self.check_line(cr, uid, vals, name, context=context)
895         return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
896
897     def write(self, cr, uid, ids, vals, context=None):
898         """ write Calendar's fields
899             @param self: The object pointer
900             @param cr: the current row, from the database cursor,
901             @param uid: the current user’s ID for security checks,
902             @param vals: Get Values
903             @param context: A standard dictionary for contextual values
904         """
905
906         if not vals:
907             return
908         for id in ids:
909             field = self.browse(cr, uid, id, context=context)
910             name = field.name.name
911             if name in ('valarm', 'attendee'):
912                 self.check_line(cr, uid, vals, name, context=context)
913         return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
914
915 basic_calendar_fields()
916
917
918 class Event(CalDAV, osv.osv_memory):
919     _name = 'basic.calendar.event'
920     _calname = 'vevent'
921     __attribute__ = {
922         'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar  component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
923         '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.
924         'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
925         'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
926         'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
927         '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.
928         'location': None, # Use: O-1, Type: TEXT            Defines the intended venue for the activity defined by a calendar component.
929         'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
930         'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
931         'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
932         'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
933         'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
934         'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
935         'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
936         'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
937         'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
938         'recurid': None,
939         'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
940         'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
941         'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
942         'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
943         'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a  reference to contact information associated with the calendar component.
944         'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
945         'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
946         'rstatus': None,
947         'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
948                                 #  like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
949         '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
950         'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
951         'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
952         'x-prop': None,
953         'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
954         'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
955     }
956
957     def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
958         """ Export calendar
959             @param self: The object pointer
960             @param cr: the current row, from the database cursor,
961             @param uid: the current user’s ID for security checks,
962             @param datas: Get datas
963             @param context: A standard dictionary for contextual values
964         """
965
966         return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
967
968 Event()
969
970
971 class ToDo(CalDAV, osv.osv_memory):
972     _name = 'basic.calendar.todo'
973     _calname = 'vtodo'
974
975     __attribute__ = {
976                 'class': None,
977                 'completed': None,
978                 'created': None,
979                 'description': None,
980                 'dtstamp': None,
981                 'dtstart': None,
982                 'duration': None,
983                 'due': None,
984                 'geo': None,
985                 'last-mod ': None,
986                 'location': None,
987                 'organizer': None,
988                 'percent': None,
989                 'priority': None,
990                 'recurid': None,
991                 'seq': None,
992                 'status': None,
993                 'summary': None,
994                 'uid': None,
995                 'url': None,
996                 'attach': None,
997                 'attendee': None,
998                 'categories': None,
999                 'comment': None,
1000                 'contact': None,
1001                 'exdate': None,
1002                 'exrule': None,
1003                 'rstatus': None,
1004                 'related': None,
1005                 'resources': None,
1006                 'rdate': None,
1007                 'rrule': None,
1008             }
1009
1010     def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
1011         """ Export Calendar
1012             @param self: The object pointer
1013             @param cr: the current row, from the database cursor,
1014             @param uid: the current user’s ID for security checks,
1015             @param datas: Get datas
1016             @param context: A standard dictionary for contextual values
1017         """
1018
1019         return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1020
1021 ToDo()
1022
1023
1024 class Journal(CalDAV):
1025     __attribute__ = {
1026     }
1027
1028
1029 class FreeBusy(CalDAV):
1030     __attribute__ = {
1031     'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a  reference to contact information associated with the calendar component.
1032     'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1033     'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1034     'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1035     'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1036     'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1037     'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1038     'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1039     'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1040     'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1041     'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1042     'rstatus': None,
1043     'X-prop': None,
1044     }
1045
1046
1047 class Timezone(CalDAV, osv.osv_memory):
1048     _name = 'basic.calendar.timezone'
1049     _calname = 'vtimezone'
1050
1051     __attribute__ = {
1052     'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1053     '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.
1054     '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.
1055     'standardc': {'tzprop': None}, # Use: R-1,
1056     'daylightc': {'tzprop': None}, # Use: R-1,
1057     'x-prop': None, # Use: O-n, Type: Text,
1058     }
1059
1060     def get_name_offset(self, cr, uid, tzid, context=None):
1061         """ Get Name Offset value
1062             @param self: The object pointer
1063             @param cr: the current row, from the database cursor,
1064             @param uid: the current user’s ID for security checks,
1065             @param context: A standard dictionary for contextual values
1066         """
1067
1068         mytz = pytz.timezone(tzid.title())
1069         mydt = datetime.now(tz=mytz)
1070         offset = mydt.utcoffset()
1071         val = offset.days * 24 + float(offset.seconds) / 3600
1072         realoffset = '%02d%02d' % (math.floor(abs(val)), \
1073                                  round(abs(val) % 1 + 0.01, 2) * 60)
1074         realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1075         return (mydt.tzname(), realoffset)
1076
1077     def export_cal(self, cr, uid, model, tzid, ical, context=None):
1078         """ Export Calendar
1079             @param self: The object pointer
1080             @param cr: the current row, from the database cursor,
1081             @param uid: the current user’s ID for security checks,
1082             @param model: Get Model's name
1083             @param context: A standard dictionary for contextual values
1084         """
1085         if context is None:
1086             context = {}
1087         ctx = context.copy()
1088         ctx.update({'model': model})
1089         cal_tz = ical.add('vtimezone')
1090         cal_tz.add('TZID').value = tzid.title()
1091         tz_std = cal_tz.add('STANDARD')
1092         tzname, offset = self.get_name_offset(cr, uid, tzid)
1093         tz_std.add("TZOFFSETFROM").value = offset
1094         tz_std.add("TZOFFSETTO").value = offset
1095         #TODO: Get start date for timezone
1096         tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1097         tz_std.add("TZNAME").value = tzname
1098         return ical
1099
1100     def import_cal(self, cr, uid, ical_data, context=None):
1101         """ Import Calendar
1102             @param self: The object pointer
1103             @param cr: the current row, from the database cursor,
1104             @param uid: the current user’s ID for security checks,
1105             @param ical_data: Get calendar's data
1106             @param context: A standard dictionary for contextual values
1107         """
1108
1109         for child in ical_data.getChildren():
1110             if child.name.lower() == 'tzid':
1111                 tzname = child.value
1112                 self.ical_set(child.name.lower(), tzname, 'value')
1113         vals = map_data(cr, uid, self, context=context)
1114         return vals
1115
1116 Timezone()
1117
1118
1119 class Alarm(CalDAV, osv.osv_memory):
1120     _name = 'basic.calendar.alarm'
1121     _calname = 'alarm'
1122
1123     __attribute__ = {
1124     'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1125     '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
1126     'summary': None, # Use: R-1, Type: Text        Which contains the text to be used as the message subject. Use for EMAIL
1127     '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
1128     '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
1129     '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
1130     '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
1131     '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.
1132     'x-prop': None,
1133     }
1134
1135     def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1136         """ Export Calendar
1137             @param self: The object pointer
1138             @param cr: the current row, from the database cursor,
1139             @param uid: the current user’s ID for security checks,
1140             @param model: Get Model's name
1141             @param alarm_id: Get Alarm's Id
1142             @param context: A standard dictionary for contextual values
1143         """
1144         valarm = vevent.add('valarm')
1145         alarm_object = self.pool.get(model)
1146         alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1147
1148         # Compute trigger data
1149         interval = alarm_data['trigger_interval']
1150         occurs = alarm_data['trigger_occurs']
1151         duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1152                                         or -(alarm_data['trigger_duration'])
1153         related = alarm_data['trigger_related']
1154         trigger = valarm.add('TRIGGER')
1155         trigger.params['related'] = [related.upper()]
1156         if interval == 'days':
1157             delta = timedelta(days=duration)
1158         if interval == 'hours':
1159             delta = timedelta(hours=duration)
1160         if interval == 'minutes':
1161             delta = timedelta(minutes=duration)
1162         trigger.value = delta
1163
1164         # Compute other details
1165         valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1166         valarm.add('ACTION').value = alarm_data['action']
1167         return vevent
1168
1169     def import_cal(self, cr, uid, ical_data, context=None):
1170         """ Import Calendar
1171             @param self: The object pointer
1172             @param cr: the current row, from the database cursor,
1173             @param uid: the current user’s ID for security checks,
1174             @param ical_data: Get calendar's Data
1175             @param context: A standard dictionary for contextual values
1176         """
1177         if context is None:
1178             context = {}
1179         ctx = context.copy()
1180         ctx.update({'model': context.get('model', None)})
1181         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1182         for child in ical_data.getChildren():
1183             if child.name.lower() == 'trigger':
1184                 if isinstance(child.value, timedelta):
1185                     seconds = child.value.seconds
1186                     days = child.value.days
1187                     diff = (days * 86400) +  seconds
1188                     interval = 'days'
1189                     related = 'before'
1190                 elif isinstance(child.value, datetime):
1191                     # TODO
1192                     # remember, spec says this datetime is in UTC
1193                     raise NotImplementedError("we cannot parse absolute triggers")
1194                 if not seconds:
1195                     duration = abs(days)
1196                     related = days > 0 and 'after' or 'before'
1197                 elif (abs(diff) / 3600) == 0:
1198                     duration = abs(diff / 60)
1199                     interval = 'minutes'
1200                     related = days >= 0 and 'after' or 'before'
1201                 else:
1202                     duration = abs(diff / 3600)
1203                     interval = 'hours'
1204                     related = days >= 0 and 'after' or 'before'
1205                 self.ical_set('trigger_interval', interval, 'value')
1206                 self.ical_set('trigger_duration', duration, 'value')
1207                 self.ical_set('trigger_occurs', related.lower(), 'value')
1208                 if child.params:
1209                     if child.params.get('related'):
1210                         self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1211             else:
1212                 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1213         vals = map_data(cr, uid, self, context=context)
1214         return vals
1215
1216 Alarm()
1217
1218
1219 class Attendee(CalDAV, osv.osv_memory):
1220     _name = 'basic.calendar.attendee'
1221     _calname = 'attendee'
1222
1223     __attribute__ = {
1224     'cutype': None, # Use: 0-1    Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1225     'member': None, # Use: 0-1    Specify the group or list membership of the calendar user specified by the property.
1226     '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"
1227     '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".
1228     '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.
1229     'delegated-to': None, # Use: 0-1    Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1230     'delegated-from': None, # Use: 0-1    Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1231     'sent-by': None, # Use: 0-1    Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1232     'cn': None, # Use: 0-1    Specify the common name to be associated with the calendar user specified by the property.
1233     'dir': None, # Use: 0-1    Specify reference to a directory entry associated with the calendar user specified by the property.
1234     'language': None, # Use: 0-1    Specify the language for text values in a property or property parameter.
1235     }
1236
1237     def import_cal(self, cr, uid, ical_data, context=None):
1238         """ Import Calendar
1239             @param self: The object pointer
1240             @param cr: the current row, from the database cursor,
1241             @param uid: the current user’s ID for security checks,
1242             @param ical_data: Get calendar's Data
1243             @param context: A standard dictionary for contextual values
1244         """
1245         if context is None:
1246             context = {}
1247         ctx = context.copy()
1248         ctx.update({'model': context.get('model', None)})
1249         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1250         for para in ical_data.params:
1251             if para.lower() == 'cn':
1252                 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1253                         ical_data.value, 'value')
1254             else:
1255                 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1256         if not ical_data.params.get('CN'):
1257             self.ical_set('cn', ical_data.value, 'value')
1258         vals = map_data(cr, uid, self, context=context)
1259         return vals
1260
1261     def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1262         """ Export Calendar
1263             @param self: The object pointer
1264             @param cr: the current row, from the database cursor,
1265             @param uid: the current user’s ID for security checks,
1266             @param model: Get model's name
1267             @param attendee_ids: Get Attendee's Id
1268             @param context: A standard dictionary for contextual values
1269         """
1270         if context is None:
1271             context = {}
1272         attendee_object = self.pool.get(model)
1273         ctx = context.copy()
1274         ctx.update({'model': model})
1275         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1276         for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1277             attendee_add = vevent.add('attendee')
1278             cn_val = ''
1279             for a_key, a_val in self.__attribute__.items():
1280                 if attendee[a_val['field']] and a_val['field'] != 'cn':
1281                     if a_val['type'] in ('text', 'char', 'selection'):
1282                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1283                     elif a_val['type'] == 'boolean':
1284                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1285                 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1286                     cn_val = [str(attendee[a_val['field']])]
1287                     if cn_val:
1288                         attendee_add.params['CN'] = cn_val
1289             if not attendee['email']:
1290                 attendee_add.value = 'MAILTO:'
1291                 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1292             elif attendee['email']:
1293                 attendee_add.value = 'MAILTO:' + attendee['email']
1294         return vevent
1295
1296 Attendee()
1297
1298 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: