Launchpad automatic translations update.
[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 not context:
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):
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 == 'hours':
168             res[attr]['type'] = "timedelta"
169         if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
170             res[attr]['object'] = field.field_id.relation
171         elif res[attr]['type'] in ('selection') and field.mapping:
172             res[attr]['mapping'] = eval(field.mapping)
173     if not res.get('uid', None):
174         res['uid'] = {}
175         res['uid']['field'] = 'id'
176         res['uid']['type'] = "integer"
177     return res
178
179 def map_data(cr, uid, obj, context=None):
180     """ Map Data
181         @param self: The object pointer
182         @param cr: the current row, from the database cursor,"""
183
184     vals = {}
185     for map_dict in obj.__attribute__:
186         map_val = obj.ical_get(map_dict, 'value')
187         field = obj.ical_get(map_dict, 'field')
188         field_type = obj.ical_get(map_dict, 'type')
189         if field:
190             if field_type == 'selection':
191                 if not map_val:
192                     continue
193                 if type(map_val) == list and len(map_val): #TOFIX: why need to check this
194                     map_val = map_val[0]
195                 mapping = obj.__attribute__[map_dict].get('mapping', False)
196                 if mapping:
197                     map_val = mapping.get(map_val.lower(), False)
198                 else:
199                     map_val = map_val.lower()
200             if field_type == 'many2many':
201                 ids = []
202                 if not map_val:
203                     vals[field] = ids
204                     continue
205                 model = obj.__attribute__[map_dict].get('object', False)
206                 modobj = obj.pool.get(model)
207                 for map_vall in map_val:
208                     id = modobj.create(cr, uid, map_vall, context=context)
209                     ids.append(id)
210                 vals[field] = [(6, 0, ids)]
211                 continue
212             if field_type == 'many2one':
213                 id = None
214                 if not map_val or not isinstance(map_val, dict):
215                     vals[field] = id
216                     continue
217                 model = obj.__attribute__[map_dict].get('object', False)
218                 modobj = obj.pool.get(model)
219                 # check if the record exists or not
220                 key1 = map_val.keys()
221                 value1 = map_val.values()
222                 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
223                 exist_id = modobj.search(cr, uid, domain, context=context)
224                 if exist_id:
225                     id = exist_id[0]
226                 else:
227                     id = modobj.create(cr, uid, map_val, context=context)
228                 vals[field] = id
229                 continue
230             if field_type == 'timedelta':
231                 if map_val:
232                     vals[field] = (map_val.seconds/float(86400) + map_val.days)
233             vals[field] = map_val
234     return vals
235
236 class CalDAV(object):
237     __attribute__ = {}
238     _logger = logging.getLogger('document.caldav')
239
240     def ical_set(self, name, value, type):
241         """ set calendar Attribute
242          @param self: The object pointer,
243          @param name: Get Attribute Name
244          @param value: Get Attribute Value
245          @param type: Get Attribute Type
246         """
247         if name in self.__attribute__ and self.__attribute__[name]:
248             self.__attribute__[name][type] = value
249         return True
250
251     def ical_get(self, name, type):
252         """ Get calendar Attribute
253          @param self: The object pointer,
254          @param name: Get Attribute Name
255          @param type: Get Attribute Type
256         """
257
258         if self.__attribute__.get(name):
259             val = self.__attribute__.get(name).get(type, None)
260             valtype =  self.__attribute__.get(name).get('type', None)
261             if type == 'value':
262                 if valtype and valtype == 'datetime' and val:
263                     if isinstance(val, list):
264                         val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
265                     else:
266                         val = val.strftime('%Y-%m-%d %H:%M:%S')
267             return  val
268         else:
269             return  self.__attribute__.get(name, None)
270
271     def ical_reset(self, type):
272         """ Reset Calendar Attribute
273          @param self: The object pointer,
274          @param type: Get Attribute Type
275         """
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                         elif map_type == "timedelta":
450                             vevent.add(field).value = timedelta(hours=data[map_field])
451                         elif map_type == "many2one":
452                             vevent.add(field).value = tools.ustr(data.get(map_field)[1])
453                         elif map_type in ("float", "integer"):
454                             vevent.add(field).value = str(data.get(map_field))
455                         elif map_type == "selection":
456                             if not self.ical_get(field, 'mapping'):
457                                 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
458                             else:
459                                 for key1, val1 in self.ical_get(field, 'mapping').items():
460                                     if val1 == data[map_field]:
461                                         vevent.add(field).value = key1.upper()
462         return vevent
463
464     def check_import(self, cr, uid, vals, context=None):
465         """
466             @param self: The object pointer
467             @param cr: the current row, from the database cursor,
468             @param uid: the current user’s ID for security checks,
469             @param vals: Get Values
470             @param context: A standard dictionary for contextual values
471         """
472         if not context:
473             context = {}
474         ids = []
475         model_obj = self.pool.get(context.get('model'))
476         recur_pool = {}
477         try:
478             for val in vals:
479                 # Compute value of duration
480                 if 'date_deadline' in val and 'duration' not in val:
481                     start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
482                     end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
483                     diff = end - start
484                     val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
485                 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
486                                                                  val.get('recurrent_id'))
487                 if val.has_key('create_date'):
488                     val.pop('create_date')
489                 u_id = val.get('id', None)
490                 val.pop('id')
491                 if exists and r_id:
492                     val.update({'recurrent_uid': exists})
493                     model_obj.write(cr, uid, [r_id], val)
494                     ids.append(r_id)
495                 elif exists:
496                     model_obj.write(cr, uid, [exists], val)
497                     ids.append(exists)
498                 else:
499                     if u_id in recur_pool and val.get('recurrent_id'):
500                         val.update({'recurrent_uid': recur_pool[u_id]})
501                         revent_id = model_obj.create(cr, uid, val)
502                         ids.append(revent_id)
503                     else:
504                         __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
505                         wematch = __rege.match(u_id.encode('utf8'))
506                         if wematch:
507                             model, recur_id, dbname = wematch.groups()
508                             val.update({'recurrent_uid': recur_id})
509                         event_id = model_obj.create(cr, uid, val)
510                         recur_pool[u_id] = event_id
511                         ids.append(event_id)
512         except Exception:
513             raise
514         return ids
515
516     def export_cal(self, cr, uid, datas, vobj=None, context=None):
517         """ Export Calendar
518             @param self: The object pointer
519             @param cr: the current row, from the database cursor,
520             @param uid: the current user’s ID for security checks,
521             @param datas: Get Data's for caldav
522             @param context: A standard dictionary for contextual values
523         """
524         try:
525             self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
526             ical = vobject.iCalendar()
527             self.create_ics(cr, uid, datas, vobj, ical, context=context)
528             return ical
529         except:
530             raise  # osv.except_osv(('Error !'), (str(e)))
531
532     def import_cal(self, cr, uid, content, data_id=None, context=None):
533         """ Import Calendar
534             @param self: The object pointer
535             @param cr: the current row, from the database cursor,
536             @param uid: the current user’s ID for security checks,
537             @param data_id: Get Data’s ID or False
538             @param context: A standard dictionary for contextual values
539         """
540
541         ical_data = content
542         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
543         parsedCal = vobject.readOne(ical_data)
544         res = []
545         vals = {}
546         for child in parsedCal.getChildren():
547             if child.name.lower() in ('vevent', 'vtodo'):
548                 vals = self.parse_ics(cr, uid, child, context=context)
549             else:
550                 vals = {}
551                 continue
552             if vals: res.append(vals)
553             self.ical_reset('value')
554         return res
555
556 class Calendar(CalDAV, osv.osv):
557     _name = 'basic.calendar'
558     _calname = 'calendar'
559
560     __attribute__ = {
561         'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
562         'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
563                            #             or the minimum and maximum range of the iCalendar specification
564                            #             that is required in order to interpret the iCalendar object.
565         'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
566         'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
567         'vevent': None, # Use: O-n, Type: Collection of Event class
568         'vtodo': None, # Use: O-n, Type: Collection of ToDo class
569         'vjournal': None, # Use: O-n, Type: Collection of Journal class
570         'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
571         'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
572     }
573     _columns = {
574             'name': fields.char("Name", size=64),
575             'user_id': fields.many2one('res.users', 'Owner'),
576             'collection_id': fields.many2one('document.directory', 'Collection', \
577                                            required=True),
578             'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
579                                     string="Type", size=64),
580             'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
581             'create_date': fields.datetime('Created Date', readonly=True),
582             'write_date': fields.datetime('Modifided Date', readonly=True),
583             'description': fields.text("description"),
584             'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
585             'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
586             'has_webcal': fields.boolean('WebCal', required=True, help="Also export a <name>.ics entry next to the calendar folder, with WebCal content."),
587     }
588     
589     _defaults = {
590         'has_webcal': False,
591     }
592
593     def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
594         if not context:
595             context = {}
596         if not domain:
597             domain = []
598         res = []
599         ctx_res_id = context.get('res_id', None)
600         ctx_model = context.get('model', None)
601         for cal in self.browse(cr, uid, ids):
602             for line in cal.line_ids:
603                 if ctx_model and ctx_model != line.object_id.model:
604                     continue
605                 if line.name in ('valarm', 'attendee'):
606                     continue
607                 line_domain = eval(line.domain or '[]', context)
608                 line_domain += domain
609                 if ctx_res_id:
610                     line_domain += [('id','=',ctx_res_id)]
611                 mod_obj = self.pool.get(line.object_id.model)
612                 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
613                 for data in mod_obj.browse(cr, uid, data_ids, context):
614                     ctx = parent and parent.context or None
615                     if hasattr(data, 'recurrent_uid') and data.recurrent_uid:
616                         # Skip for event which is child of other event
617                         continue
618                     node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
619                     res.append(node)
620         return res
621         
622
623     def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
624         if not context:
625             context = {}
626         if not domain:
627             domain = []
628         res = None
629         ctx_res_id = context.get('res_id', None)
630         ctx_model = context.get('model', None)
631         for cal in self.browse(cr, uid, ids):
632             for line in cal.line_ids:
633                 if ctx_model and ctx_model != line.object_id.model:
634                     continue
635                 if line.name in ('valarm', 'attendee'):
636                     continue
637                 line_domain = eval(line.domain or '[]', context)
638                 line_domain += domain
639                 if ctx_res_id:
640                     line_domain += [('id','=',ctx_res_id)]
641                 mod_obj = self.pool.get(line.object_id.model)
642                 max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
643                 if res and res > max_data:
644                     continue
645                 res = max_data
646         return res
647
648     def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
649         """ Export Calendar
650             @param ids: List of calendar’s IDs
651             @param vobj: the type of object to export
652             @return the ical data.
653         """
654         if not context:
655            context = {}
656         ctx_model = context.get('model', None)
657         ctx_res_id = context.get('res_id', None)
658         ical = vobject.iCalendar()
659         for cal in self.browse(cr, uid, ids):
660             for line in cal.line_ids:
661                 if ctx_model and ctx_model != line.object_id.model:
662                     continue
663                 if line.name in ('valarm', 'attendee'):
664                     continue
665                 domain = eval(line.domain or '[]', context)
666                 if ctx_res_id:
667                     domain += [('id','=',ctx_res_id)]
668                 mod_obj = self.pool.get(line.object_id.model)
669                 data_ids = mod_obj.search(cr, uid, domain, context=context)
670                 datas = mod_obj.read(cr, uid, data_ids, context=context)
671                 context.update({'model': line.object_id.model,
672                                         'calendar_id': cal.id
673                                         })
674                 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
675                 self.create_ics(cr, uid, datas, line.name, ical, context=context)
676         return ical.serialize()
677
678     def import_cal(self, cr, uid, content, data_id=None, context=None):
679         """ Import Calendar
680             @param self: The object pointer
681             @param cr: the current row, from the database cursor,
682             @param uid: the current user’s ID for security checks,
683             @param data_id: Get Data’s ID or False
684             @param context: A standard dictionary for contextual values
685         """
686         if not context:
687             context = {}
688         vals = []
689         ical_data = content
690         parsedCal = vobject.readOne(ical_data)
691         if not data_id:
692             data_id = self.search(cr, uid, [])[0]
693         cal = self.browse(cr, uid, data_id, context=context)
694         cal_children = {}
695
696         for line in cal.line_ids:
697             cal_children[line.name] = line.object_id.model
698         objs = []
699         checked = True
700         for child in parsedCal.getChildren():
701             if child.name.lower() in cal_children:
702                 context.update({'model': cal_children[child.name.lower()],
703                                 'calendar_id': cal['id']
704                                 })
705                 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
706                 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
707                 vals.append(val)
708                 objs.append(cal_children[child.name.lower()])
709             elif child.name.upper() == 'CALSCALE':
710                 if child.value.upper() != 'GREGORIAN':
711                     self._logger.warning('How do I handle %s calendars?',child.value)
712             elif child.name.upper() in ('PRODID', 'VERSION'):
713                 pass
714             elif child.name.upper().startswith('X-'):
715                 self._logger.debug("skipping custom node %s", child.name)
716             else:
717                 self._logger.debug("skipping node %s", child.name)
718         
719         res = []
720         for obj_name in list(set(objs)):
721             obj = self.pool.get(obj_name)
722             if hasattr(obj, 'check_import'):
723                 r = obj.check_import(cr, uid, vals, context=context)
724                 checked = True
725                 res.extend(r)
726
727         if not checked:
728             r = self.check_import(cr, uid, vals, context=context)
729             res.extend(r)
730         return res
731
732 Calendar()
733
734
735 class basic_calendar_line(osv.osv):
736     """ Calendar Lines """
737
738     _name = 'basic.calendar.lines'
739     _description = 'Calendar Lines'
740
741     _columns = {
742             'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
743                                     ('valarm', 'Alarm'), \
744                                     ('attendee', 'Attendee')], \
745                                     string="Type", size=64),
746             'object_id': fields.many2one('ir.model', 'Object'),
747             'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
748                                        required=True, ondelete='cascade'),
749             'domain': fields.char('Domain', size=124),
750             'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
751     }
752
753     _defaults = {
754         'domain': lambda *a: '[]',
755     }
756
757     def create(self, cr, uid, vals, context=None):
758         """ create calendar's line
759             @param self: The object pointer
760             @param cr: the current row, from the database cursor,
761             @param uid: the current user’s ID for security checks,
762             @param vals: Get the Values
763             @param context: A standard dictionary for contextual values
764         """
765
766         cr.execute("SELECT COUNT(id) FROM basic_calendar_lines \
767                                 WHERE name=%s AND calendar_id=%s", 
768                                 (vals.get('name'), vals.get('calendar_id')))
769         res = cr.fetchone()
770         if res:
771             if res[0] > 0:
772                 raise osv.except_osv(_('Warning !'), _('Can not create line "%s" more than once') % (vals.get('name')))
773         return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
774
775 basic_calendar_line()
776
777 class basic_calendar_alias(osv.osv):
778     """ Mapping of client filenames to ORM ids of calendar records
779     
780         Since some clients insist on putting arbitrary filenames on the .ics data
781         they send us, and they won't respect the redirection "Location:" header, 
782         we have to store those filenames and allow clients to call our calendar
783         records with them.
784         Note that adding a column to all tables that would possibly hold calendar-
785         mapped data won't work. The user is always allowed to specify more 
786         calendars, on any arbitrary ORM object, without need to alter those tables'
787         data or structure
788     """
789     _name = 'basic.calendar.alias'
790     _columns = {
791         'name': fields.char('Filename', size=512, required=True, select=1),
792         'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
793                         select=1, help='The calendar/line this mapping applies to'),
794         'res_id': fields.integer('Res. ID', required=True, select=1),
795         }
796         
797     _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
798                 _('The same filename cannot apply to two records!')), ]
799
800 basic_calendar_alias()
801
802 class basic_calendar_attribute(osv.osv):
803     _name = 'basic.calendar.attributes'
804     _description = 'Calendar attributes'
805     _columns = {
806         'name': fields.char("Name", size=64, required=True),
807         'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
808                                     ('alarm', 'Alarm'), \
809                                     ('attendee', 'Attendee')], \
810                                     string="Type", size=64, required=True),
811     }
812
813 basic_calendar_attribute()
814
815
816 class basic_calendar_fields(osv.osv):
817     """ Calendar fields """
818
819     _name = 'basic.calendar.fields'
820     _description = 'Calendar fields'
821
822     _columns = {
823         'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
824         'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
825         'type_id': fields.many2one('basic.calendar.lines', 'Type', \
826                                    required=True, ondelete='cascade'),
827         'expr': fields.char("Expression", size=64),
828         'fn': fields.selection([('field', 'Use the field'),
829                         ('const', 'Expression as constant'),
830                         ('hours', 'Interval in hours'),
831                         ], 'Function'),
832         'mapping': fields.text('Mapping'),
833     }
834
835     _defaults = {
836         'fn': lambda *a: 'field',
837     }
838
839     _sql_constraints = [
840         ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
841     ]
842
843     def check_line(self, cr, uid, vals, name, context=None):
844         """ check calendar's line
845             @param self: The object pointer
846             @param cr: the current row, from the database cursor,
847             @param uid: the current user’s ID for security checks,
848             @param vals: Get Values
849             @param context: A standard dictionary for contextual values
850         """
851         f_obj = self.pool.get('ir.model.fields')
852         field = f_obj.browse(cr, uid, vals['field_id'], context=context)
853         relation = field.relation
854         line_obj = self.pool.get('basic.calendar.lines')
855         l_id = line_obj.search(cr, uid, [('name', '=', name)])
856         if l_id:
857             line = line_obj.browse(cr, uid, l_id, context=context)[0]
858             line_rel = line.object_id.model
859             if (relation != 'NULL') and (not relation == line_rel):
860                 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines') % (name))
861         return True
862
863     def create(self, cr, uid, vals, context=None):
864         """ Create Calendar's fields
865             @param self: The object pointer
866             @param cr: the current row, from the database cursor,
867             @param uid: the current user’s ID for security checks,
868             @param vals: Get Values
869             @param context: A standard dictionary for contextual values
870         """
871
872         cr.execute('SELECT name FROM basic_calendar_attributes \
873                             WHERE id=%s', (vals.get('name'),))
874         name = cr.fetchone()
875         name = name[0]
876         if name in ('valarm', 'attendee'):
877             self.check_line(cr, uid, vals, name, context=context)
878         return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
879
880     def write(self, cr, uid, ids, vals, context=None):
881         """ write 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         if not vals:
890             return
891         for id in ids:
892             field = self.browse(cr, uid, id, context=context)
893             name = field.name.name
894             if name in ('valarm', 'attendee'):
895                 self.check_line(cr, uid, vals, name, context=context)
896         return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
897
898 basic_calendar_fields()
899
900
901 class Event(CalDAV, osv.osv_memory):
902     _name = 'basic.calendar.event'
903     _calname = 'vevent'
904     __attribute__ = {
905         'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar  component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
906         '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.
907         'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
908         'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
909         'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
910         '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.
911         'location': None, # Use: O-1, Type: TEXT            Defines the intended venue for the activity defined by a calendar component.
912         'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
913         'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
914         'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
915         'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
916         'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
917         'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
918         'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
919         'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
920         'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
921         'recurid': None,
922         'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
923         'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
924         'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
925         'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
926         'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a  reference to contact information associated with the calendar component.
927         'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
928         'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
929         'rstatus': None,
930         'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
931                                 #  like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
932         '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
933         'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
934         'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
935         'x-prop': None,
936         'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
937         'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
938     }
939
940     def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
941         """ Export calendar
942             @param self: The object pointer
943             @param cr: the current row, from the database cursor,
944             @param uid: the current user’s ID for security checks,
945             @param datas: Get datas
946             @param context: A standard dictionary for contextual values
947         """
948
949         return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
950
951 Event()
952
953
954 class ToDo(CalDAV, osv.osv_memory):
955     _name = 'basic.calendar.todo'
956     _calname = 'vtodo'
957
958     __attribute__ = {
959                 'class': None,
960                 'completed': None,
961                 'created': None,
962                 'description': None,
963                 'dtstamp': None,
964                 'dtstart': None,
965                 'duration': None,
966                 'due': None,
967                 'geo': None,
968                 'last-mod ': None,
969                 'location': None,
970                 'organizer': None,
971                 'percent': None,
972                 'priority': None,
973                 'recurid': None,
974                 'seq': None,
975                 'status': None,
976                 'summary': None,
977                 'uid': None,
978                 'url': None,
979                 'attach': None,
980                 'attendee': None,
981                 'categories': None,
982                 'comment': None,
983                 'contact': None,
984                 'exdate': None,
985                 'exrule': None,
986                 'rstatus': None,
987                 'related': None,
988                 'resources': None,
989                 'rdate': None,
990                 'rrule': None,
991             }
992
993     def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
994         """ Export Calendar
995             @param self: The object pointer
996             @param cr: the current row, from the database cursor,
997             @param uid: the current user’s ID for security checks,
998             @param datas: Get datas
999             @param context: A standard dictionary for contextual values
1000         """
1001
1002         return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1003
1004 ToDo()
1005
1006
1007 class Journal(CalDAV):
1008     __attribute__ = {
1009     }
1010
1011
1012 class FreeBusy(CalDAV):
1013     __attribute__ = {
1014     'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a  reference to contact information associated with the calendar component.
1015     'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1016     'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1017     'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1018     'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1019     'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1020     'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1021     'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1022     'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1023     'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1024     'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1025     'rstatus': None,
1026     'X-prop': None,
1027     }
1028
1029
1030 class Timezone(CalDAV, osv.osv_memory):
1031     _name = 'basic.calendar.timezone'
1032     _calname = 'vtimezone'
1033
1034     __attribute__ = {
1035     'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1036     '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.
1037     '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.
1038     'standardc': {'tzprop': None}, # Use: R-1,
1039     'daylightc': {'tzprop': None}, # Use: R-1,
1040     'x-prop': None, # Use: O-n, Type: Text,
1041     }
1042
1043     def get_name_offset(self, cr, uid, tzid, context=None):
1044         """ Get Name Offset value
1045             @param self: The object pointer
1046             @param cr: the current row, from the database cursor,
1047             @param uid: the current user’s ID for security checks,
1048             @param context: A standard dictionary for contextual values
1049         """
1050
1051         mytz = pytz.timezone(tzid.title())
1052         mydt = datetime.now(tz=mytz)
1053         offset = mydt.utcoffset()
1054         val = offset.days * 24 + float(offset.seconds) / 3600
1055         realoffset = '%02d%02d' % (math.floor(abs(val)), \
1056                                  round(abs(val) % 1 + 0.01, 2) * 60)
1057         realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1058         return (mydt.tzname(), realoffset)
1059
1060     def export_cal(self, cr, uid, model, tzid, ical, context=None):
1061         """ Export Calendar
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 model: Get Model's name
1066             @param context: A standard dictionary for contextual values
1067         """
1068         if not context:
1069             context = {}
1070         ctx = context.copy()
1071         ctx.update({'model': model})
1072         cal_tz = ical.add('vtimezone')
1073         cal_tz.add('TZID').value = tzid.title()
1074         tz_std = cal_tz.add('STANDARD')
1075         tzname, offset = self.get_name_offset(cr, uid, tzid)
1076         tz_std.add("TZOFFSETFROM").value = offset
1077         tz_std.add("TZOFFSETTO").value = offset
1078         #TODO: Get start date for timezone
1079         tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1080         tz_std.add("TZNAME").value = tzname
1081         return ical
1082
1083     def import_cal(self, cr, uid, ical_data, context=None):
1084         """ Import Calendar
1085             @param self: The object pointer
1086             @param cr: the current row, from the database cursor,
1087             @param uid: the current user’s ID for security checks,
1088             @param ical_data: Get calendar's data
1089             @param context: A standard dictionary for contextual values
1090         """
1091
1092         for child in ical_data.getChildren():
1093             if child.name.lower() == 'tzid':
1094                 tzname = child.value
1095                 self.ical_set(child.name.lower(), tzname, 'value')
1096         vals = map_data(cr, uid, self, context=context)
1097         return vals
1098
1099 Timezone()
1100
1101
1102 class Alarm(CalDAV, osv.osv_memory):
1103     _name = 'basic.calendar.alarm'
1104     _calname = 'alarm'
1105
1106     __attribute__ = {
1107     'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1108     '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
1109     'summary': None, # Use: R-1, Type: Text        Which contains the text to be used as the message subject. Use for EMAIL
1110     '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
1111     '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
1112     '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
1113     '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
1114     '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.
1115     'x-prop': None,
1116     }
1117
1118     def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1119         """ Export Calendar
1120             @param self: The object pointer
1121             @param cr: the current row, from the database cursor,
1122             @param uid: the current user’s ID for security checks,
1123             @param model: Get Model's name
1124             @param alarm_id: Get Alarm's Id
1125             @param context: A standard dictionary for contextual values
1126         """
1127         if not context:
1128             context = {}
1129         valarm = vevent.add('valarm')
1130         alarm_object = self.pool.get(model)
1131         alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1132
1133         # Compute trigger data
1134         interval = alarm_data['trigger_interval']
1135         occurs = alarm_data['trigger_occurs']
1136         duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1137                                         or -(alarm_data['trigger_duration'])
1138         related = alarm_data['trigger_related']
1139         trigger = valarm.add('TRIGGER')
1140         trigger.params['related'] = [related.upper()]
1141         if interval == 'days':
1142             delta = timedelta(days=duration)
1143         if interval == 'hours':
1144             delta = timedelta(hours=duration)
1145         if interval == 'minutes':
1146             delta = timedelta(minutes=duration)
1147         trigger.value = delta
1148
1149         # Compute other details
1150         valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1151         valarm.add('ACTION').value = alarm_data['action']
1152         return vevent
1153
1154     def import_cal(self, cr, uid, ical_data, context=None):
1155         """ Import Calendar
1156             @param self: The object pointer
1157             @param cr: the current row, from the database cursor,
1158             @param uid: the current user’s ID for security checks,
1159             @param ical_data: Get calendar's Data
1160             @param context: A standard dictionary for contextual values
1161         """
1162
1163         ctx = context.copy()
1164         ctx.update({'model': context.get('model', None)})
1165         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1166         for child in ical_data.getChildren():
1167             if child.name.lower() == 'trigger':
1168                 seconds = child.value.seconds
1169                 days = child.value.days
1170                 diff = (days * 86400) +  seconds
1171                 interval = 'days'
1172                 related = 'before'
1173                 if not seconds:
1174                     duration = abs(days)
1175                     related = days > 0 and 'after' or 'before'
1176                 elif (abs(diff) / 3600) == 0:
1177                     duration = abs(diff / 60)
1178                     interval = 'minutes'
1179                     related = days >= 0 and 'after' or 'before'
1180                 else:
1181                     duration = abs(diff / 3600)
1182                     interval = 'hours'
1183                     related = days >= 0 and 'after' or 'before'
1184                 self.ical_set('trigger_interval', interval, 'value')
1185                 self.ical_set('trigger_duration', duration, 'value')
1186                 self.ical_set('trigger_occurs', related.lower(), 'value')
1187                 if child.params:
1188                     if child.params.get('related'):
1189                         self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1190             else:
1191                 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1192         vals = map_data(cr, uid, self, context=context)
1193         return vals
1194
1195 Alarm()
1196
1197
1198 class Attendee(CalDAV, osv.osv_memory):
1199     _name = 'basic.calendar.attendee'
1200     _calname = 'attendee'
1201
1202     __attribute__ = {
1203     'cutype': None, # Use: 0-1    Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1204     'member': None, # Use: 0-1    Specify the group or list membership of the calendar user specified by the property.
1205     '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"
1206     '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".
1207     '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.
1208     'delegated-to': None, # Use: 0-1    Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1209     'delegated-from': None, # Use: 0-1    Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1210     'sent-by': None, # Use: 0-1    Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1211     'cn': None, # Use: 0-1    Specify the common name to be associated with the calendar user specified by the property.
1212     'dir': None, # Use: 0-1    Specify reference to a directory entry associated with the calendar user specified by the property.
1213     'language': None, # Use: 0-1    Specify the language for text values in a property or property parameter.
1214     }
1215
1216     def import_cal(self, cr, uid, ical_data, context=None):
1217         """ Import Calendar
1218             @param self: The object pointer
1219             @param cr: the current row, from the database cursor,
1220             @param uid: the current user’s ID for security checks,
1221             @param ical_data: Get calendar's Data
1222             @param context: A standard dictionary for contextual values
1223         """
1224
1225         ctx = context.copy()
1226         ctx.update({'model': context.get('model', None)})
1227         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1228         for para in ical_data.params:
1229             if para.lower() == 'cn':
1230                 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1231                         ical_data.value, 'value')
1232             else:
1233                 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1234         if not ical_data.params.get('CN'):
1235             self.ical_set('cn', ical_data.value, 'value')
1236         vals = map_data(cr, uid, self, context=context)
1237         return vals
1238
1239     def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1240         """ Export Calendar
1241             @param self: The object pointer
1242             @param cr: the current row, from the database cursor,
1243             @param uid: the current user’s ID for security checks,
1244             @param model: Get model's name
1245             @param attendee_ids: Get Attendee's Id
1246             @param context: A standard dictionary for contextual values
1247         """
1248         if not context:
1249             context = {}
1250         attendee_object = self.pool.get(model)
1251         ctx = context.copy()
1252         ctx.update({'model': model})
1253         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1254         for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1255             attendee_add = vevent.add('attendee')
1256             cn_val = ''
1257             for a_key, a_val in self.__attribute__.items():
1258                 if attendee[a_val['field']] and a_val['field'] != 'cn':
1259                     if a_val['type'] in ('text', 'char', 'selection'):
1260                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1261                     elif a_val['type'] == 'boolean':
1262                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1263                 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1264                     cn_val = [str(attendee[a_val['field']])]
1265                     if cn_val:
1266                         attendee_add.params['CN'] = cn_val
1267             if not attendee['email']:
1268                 attendee_add.value = 'MAILTO:'
1269                 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1270             elif attendee['email']:
1271                 attendee_add.value = 'MAILTO:' + attendee['email']
1272         return vevent
1273
1274 Attendee()
1275
1276 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: