[IMP]:caldav: If we clear some value from calendar client it should reflect on OpenER...
[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 base64
28 import math
29 import pooler
30 import pytz
31 import re
32 import tools
33 import time
34
35 try:
36     import vobject
37 except ImportError:
38     raise osv.except_osv('vobject Import Error!','Please install python-vobject \
39                      from http://vobject.skyhouseconsulting.com/')
40
41 # O-1  Optional and can come only once
42 # O-n  Optional and can come more than once
43 # R-1  Required and can come only once
44 # R-n  Required and can come more than once
45
46 def uid2openobjectid(cr, uidval, oomodel, rdate):
47     """ UID To Open Object Id
48         @param cr: the current row, from the database cursor,
49         @param uidval: Get USerId vale
50         @oomodel: Open Object ModelName
51         @param rdate: Get Recurrent Date
52     """
53     __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
54     wematch = __rege.match(uidval.encode('utf8'))
55     if not wematch:
56         return (False, None)
57     else:
58         model, id, dbname = wematch.groups()
59         model_obj = pooler.get_pool(cr.dbname).get(model)
60         if (not model == oomodel) or (not dbname == cr.dbname):
61             return (False, None)
62         qry = 'select distinct(id) from %s' % model_obj._table
63         if rdate:
64             qry += " where recurrent_id='%s'" % (rdate)
65             cr.execute(qry)
66             r_id = cr.fetchone()
67             if r_id:
68                 return (id, r_id[0])
69         cr.execute(qry)
70         ids = map(lambda x: str(x[0]), cr.fetchall())
71         if id in ids:
72             return (id, None)
73         return (False, None)
74
75 def openobjectid2uid(cr, uidval, oomodel):
76     """ Open Object Id To UId
77         @param cr: the current row, from the database cursor,
78         @param uidval: Get USerId vale
79         @oomodel: Open Object ModelName """
80
81     value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
82     return value
83
84 def get_attribute_mapping(cr, uid, calname, context={}):
85     """ Attribute Mapping with Basic calendar fields and lines
86         @param cr: the current row, from the database cursor,
87         @param uid: the current user’s ID for security checks,
88         @param calname: Get Calendar name
89         @param context: A standard dictionary for contextual values """
90
91     if not context:
92         context = {}
93     pool = pooler.get_pool(cr.dbname)
94     field_obj = pool.get('basic.calendar.fields')
95     type_obj = pool.get('basic.calendar.lines')
96     domain = [('object_id.model', '=', context.get('model'))]
97     if context.get('calendar_id'):
98         domain.append(('calendar_id', '=', context.get('calendar_id')))
99     type_id = type_obj.search(cr, uid, domain)
100     fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])])
101     res = {}
102     for field in field_obj.browse(cr, uid, fids):
103         attr = field.name.name
104         res[attr] = {}
105         res[attr]['field'] = field.field_id.name
106         res[attr]['type'] = field.field_id.ttype
107         if field.fn == 'hours':
108             res[attr]['type'] = "timedelta"
109         if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
110             res[attr]['object'] = field.field_id.relation
111         elif res[attr]['type'] in ('selection') and field.mapping:
112             res[attr]['mapping'] = eval(field.mapping)
113     if not res.get('uid', None):
114         res['uid'] = {}
115         res['uid']['field'] = 'id'
116         res['uid']['type'] = "integer"
117     return res
118
119 def map_data(cr, uid, obj):
120     """ Map Data
121         @param self: The object pointer
122         @param cr: the current row, from the database cursor,"""
123
124     vals = {}
125     for map_dict in obj.__attribute__:
126         map_val = obj.ical_get(map_dict, 'value')
127         field = obj.ical_get(map_dict, 'field')
128         field_type = obj.ical_get(map_dict, 'type')
129         if field:
130             if field_type == 'selection':
131                 if not map_val:
132                     continue
133                 mapping = obj.__attribute__[map_dict].get('mapping', False)
134                 if mapping:
135                     map_val = mapping[map_val.lower()]
136                 else:
137                     map_val = map_val.lower()
138             if field_type == 'many2many':
139                 ids = []
140                 if not map_val:
141                     vals[field] = ids
142                     continue
143                 model = obj.__attribute__[map_dict].get('object', False)
144                 modobj = obj.pool.get(model)
145                 for map_vall in map_val:
146                     id = modobj.create(cr, uid, map_vall)
147                     ids.append(id)
148                 vals[field] = [(6, 0, ids)]
149                 continue
150             if field_type == 'many2one':
151                 id = None
152                 if not map_val or not isinstance(map_val, dict):
153                     vals[field] = id
154                     continue
155                 model = obj.__attribute__[map_dict].get('object', False)
156                 modobj = obj.pool.get(model)
157                 id = modobj.create(cr, uid, map_val)
158                 vals[field] = id
159                 continue
160             if field_type == 'timedelta':
161                 if map_val:
162                     vals[field] = (map_val.seconds/float(86400) + map_val.days)
163             vals[field] = map_val
164     return vals
165
166 class CalDAV(object):
167     __attribute__ = {}
168
169
170
171     def ical_set(self, name, value, type):
172         """ set calendar Attribute
173          @param self: The object pointer,
174          @param name: Get Attribute Name
175          @param value: Get Attribute Value
176          @param type: Get Attribute Type
177         """
178         if name in self.__attribute__ and self.__attribute__[name]:
179             self.__attribute__[name][type] = value
180         return True
181
182     def ical_get(self, name, type):
183         """ Get calendar Attribute
184          @param self: The object pointer,
185          @param name: Get Attribute Name
186          @param type: Get Attribute Type
187         """
188
189         if self.__attribute__.get(name):
190             val = self.__attribute__.get(name).get(type, None)
191             valtype =  self.__attribute__.get(name).get('type', None)
192             if type == 'value':
193                 if valtype and valtype == 'datetime' and val:
194                     if isinstance(val, list):
195                         val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
196                     else:
197                         val = val.strftime('%Y-%m-%d %H:%M:%S')
198             return  val
199         else:
200             return  self.__attribute__.get(name, None)
201
202     def ical_reset(self, type):
203         """ Reset Calendar Attribute
204          @param self: The object pointer,
205          @param type: Get Attribute Type
206         """
207
208         for name in self.__attribute__:
209             if self.__attribute__[name]:
210                 self.__attribute__[name][type] = None
211         return True
212
213     def parse_ics(self, cr, uid, child, cal_children=None, context=None):
214         """ parse calendaring and scheduling information
215         @param self: The object pointer
216         @param cr: the current row, from the database cursor,
217         @param uid: the current user’s ID for security checks,
218         @param context: A standard dictionary for contextual values """
219
220         att_data = []
221         for cal_data in child.getChildren():
222             if cal_data.name.lower() == 'attendee':
223                 ctx = context.copy()
224                 if cal_children:
225                     ctx.update({'model': cal_children[cal_data.name.lower()]})
226                 attendee = self.pool.get('basic.calendar.attendee')
227                 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
228                 self.ical_set(cal_data.name.lower(), att_data, 'value')
229                 continue
230             if cal_data.name.lower() == 'valarm':
231                 alarm = self.pool.get('basic.calendar.alarm')
232                 ctx = context.copy()
233                 if cal_children:
234                     ctx.update({'model': cal_children[cal_data.name.lower()]})
235                 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
236                 self.ical_set(cal_data.name.lower(), vals, 'value')
237                 continue
238             if cal_data.name.lower() == 'exdate':
239                 exval = map(lambda x: str(x), cal_data.value)
240                 self.ical_set(cal_data.name.lower(), ','.join(exval), 'value')
241                 continue
242             if cal_data.name.lower() in self.__attribute__:
243
244                 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
245                     self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
246                 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
247         vals = map_data(cr, uid, self)
248         return vals
249
250     def create_ics(self, cr, uid, datas, name, ical, context=None):
251         """ create calendaring and scheduling information
252         @param self: The object pointer
253         @param cr: the current row, from the database cursor,
254         @param uid: the current user’s ID for security checks,
255         @param context: A standard dictionary for contextual values """
256
257         if not datas:
258             return
259         timezones = []
260         for data in datas:
261             tzval = None
262             vevent = ical.add(name)
263             for field in self.__attribute__.keys():
264                 map_field = self.ical_get(field, 'field')
265                 map_type = self.ical_get(field, 'type')
266                 if map_field in data.keys():
267                     if field == 'uid':
268                         model = context.get('model', None)
269                         if not model:
270                             continue
271                         uidval = openobjectid2uid(cr, data[map_field], model)
272                         model_obj = self.pool.get(model)
273                         r_ids = []
274                         if model_obj._columns.get('recurrent_uid', None):
275                             cr.execute('select id from %s  where recurrent_uid=%s'
276                                            % (model_obj._table, data[map_field]))
277                             r_ids = map(lambda x: x[0], cr.fetchall())
278                         if r_ids:
279                             rdata = self.pool.get(model).read(cr, uid, r_ids)
280                             event_obj = self.pool.get('basic.calendar.event')
281                             rcal = event_obj.export_cal(cr, uid, rdata, context=context)
282                             for revents in rcal.contents['vevent']:
283                                 ical.contents['vevent'].append(revents)
284                         if data.get('recurrent_uid', None):
285                             uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
286                         vevent.add('uid').value = uidval
287                     elif field == 'attendee' and data[map_field]:
288                         model = self.__attribute__[field].get('object', False)
289                         attendee_obj = self.pool.get('basic.calendar.attendee')
290                         vevent = attendee_obj.export_cal(cr, uid, model, \
291                                      data[map_field], vevent, context=context)
292                     elif field == 'valarm' and data[map_field]:
293                         model = self.__attribute__[field].get('object', False)
294                         ctx = context.copy()
295                         ctx.update({'model': model})
296                         alarm_obj = self.pool.get('basic.calendar.alarm')
297                         vevent = alarm_obj.export_cal(cr, uid, model, \
298                                     data[map_field][0], vevent, context=ctx)
299                     elif field == 'vtimezone' and data[map_field] and data[map_field] not in timezones:
300                         tzval = data[map_field]
301                         tz_obj = self.pool.get('basic.calendar.timezone')
302                         ical = tz_obj.export_cal(cr, uid, None, \
303                                      data[map_field], ical, context=context)
304                         timezones.append(data[map_field])
305                     elif data[map_field]:
306                         if map_type in ("char", "text"):
307                             if field in ('exdate'):
308                                 vevent.add(field).value = map(parser.parse, (data[map_field]).split(','))
309                             else:
310                                 vevent.add(field).value = tools.ustr(data[map_field])
311                         elif map_type in ('datetime', 'date') and data[map_field]:
312                             dtfield = vevent.add(field)
313                             dtfield.value = parser.parse(data[map_field])
314                             if tzval:
315                                 dtfield.params['TZID'] = [tzval.title()]
316                         elif map_type == "timedelta":
317                             vevent.add(field).value = timedelta(hours=data[map_field])
318                         elif map_type == "many2one":
319                             vevent.add(field).value = tools.ustr(data.get(map_field)[1])
320                         elif map_type in ("float", "integer"):
321                             vevent.add(field).value = str(data.get(map_field))
322                         elif map_type == "selection":
323                             if not self.ical_get(field, 'mapping'):
324                                 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
325                             else:
326                                 for key1, val1 in self.ical_get(field, 'mapping').items():
327                                     if val1 == data[map_field]:
328                                         vevent.add(field).value = key1
329         return vevent
330
331     def check_import(self, cr, uid, vals, context={}):
332         """
333             @param self: The object pointer
334             @param cr: the current row, from the database cursor,
335             @param uid: the current user’s ID for security checks,
336             @param vals: Get Values
337             @param context: A standard dictionary for contextual values
338         """
339
340         ids = []
341         model_obj = self.pool.get(context.get('model'))
342         recur_pool = {}
343         try:
344             for val in vals:
345                 exists, r_id = uid2openobjectid(cr, val['id'], context.get('model'), \
346                                                                  val.get('recurrent_id'))
347                 if val.has_key('create_date'): val.pop('create_date')
348                 u_id = val.get('id', None)
349                 val.pop('id')
350                 if exists and r_id:
351                     val.update({'recurrent_uid': exists})
352                     model_obj.write(cr, uid, [r_id], val)
353                     ids.append(r_id)
354                 elif exists:
355                     model_obj.write(cr, uid, [exists], val)
356                     ids.append(exists)
357                 else:
358                     if u_id in recur_pool and val.get('recurrent_id'):
359                         val.update({'recurrent_uid': recur_pool[u_id]})
360                         revent_id = model_obj.create(cr, uid, val)
361                         ids.append(revent_id)
362                     else:
363                         event_id = model_obj.create(cr, uid, val)
364                         recur_pool[u_id] = event_id
365                         ids.append(event_id)
366         except Exception, e:
367             raise osv.except_osv(('Error !'), (str(e)))
368         return ids
369
370     def export_cal(self, cr, uid, datas, vobj=None, context={}):
371         """ Export Calendar
372             @param self: The object pointer
373             @param cr: the current row, from the database cursor,
374             @param uid: the current user’s ID for security checks,
375             @param datas: Get Data's for caldav
376             @param context: A standard dictionary for contextual values
377         """
378
379         try:
380             self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
381             ical = vobject.iCalendar()
382             self.create_ics(cr, uid, datas, vobj, ical, context=context)
383             return ical
384         except Exception, e:
385             raise osv.except_osv(('Error !'), (str(e)))
386
387     def import_cal(self, cr, uid, content, data_id=None, context=None):
388         """ Import Calendar
389             @param self: The object pointer
390             @param cr: the current row, from the database cursor,
391             @param uid: the current user’s ID for security checks,
392             @param data_id: Get Data’s ID or False
393             @param context: A standard dictionary for contextual values
394         """
395
396         ical_data = base64.decodestring(content)
397         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
398         parsedCal = vobject.readOne(ical_data)
399         res = []
400         vals = {}
401         for child in parsedCal.getChildren():
402             if child.name.lower() in ('vevent', 'vtodo'):
403                 vals = self.parse_ics(cr, uid, child, context=context)
404             else:
405                 vals = {}
406                 continue
407             if vals: res.append(vals)
408             self.ical_reset('value')
409         return res
410
411 class Calendar(CalDAV, osv.osv):
412     _name = 'basic.calendar'    
413     _calname = 'calendar'
414
415     __attribute__ = {
416         'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
417         'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
418                            #             or the minimum and maximum range of the iCalendar specification
419                            #             that is required in order to interpret the iCalendar object.
420         'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
421         'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
422         'vevent': None, # Use: O-n, Type: Collection of Event class
423         'vtodo': None, # Use: O-n, Type: Collection of ToDo class
424         'vjournal': None, # Use: O-n, Type: Collection of Journal class
425         'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
426         'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
427     }
428     _columns = {
429             'name': fields.char("Name", size=64),            
430             'user_id': fields.many2one('res.users', 'Owner'),
431             'collection_id': fields.many2one('document.directory', 'Collection', \
432                                            required=True),
433             'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
434                                     string="Type", size=64),
435             'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),            
436             'create_date': fields.datetime('Created Date'),
437             'write_date': fields.datetime('Modifided Date'),
438     }   
439
440     def get_calendar_object(self, cr, uid, uri, context=None):
441         if not uri:
442             return None
443         if len(uri) > 1:
444             return None
445         name, file_type = tuple(uri[0].split('.'))
446         res = self.name_search(cr, uid, name)
447         if not res:
448             return None
449         calendar_id, calendar_name = res[0]
450         calendar = self.browse(cr, uid, calendar_id)
451         return node_calendar(uri, context, calendar)
452
453     def export_cal(self, cr, uid, ids, vobj='vevent', context={}):
454         """ Export Calendar
455             @param self: The object pointer
456             @param cr: the current row, from the database cursor,
457             @param uid: the current user’s ID for security checks,
458             @param ids: List of calendar’s IDs
459             @param context: A standard dictionary for contextual values
460         """
461
462         cal = self.browse(cr, uid, ids[0])
463         ical = vobject.iCalendar()
464         for line in cal.line_ids:
465             if line.name in ('valarm', 'attendee'):
466                 continue
467             mod_obj = self.pool.get(line.object_id.model)
468             data_ids = mod_obj.search(cr, uid, eval(line.domain), context=context)
469             datas = mod_obj.read(cr, uid, data_ids, context=context)
470             context.update({'model': line.object_id.model,
471                                     'calendar_id': cal.id
472                                     })
473             self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
474             self.create_ics(cr, uid, datas, line.name, ical, context=context)
475         return ical.serialize()
476
477     def import_cal(self, cr, uid, content, data_id=None, context=None):
478         """ Import Calendar
479             @param self: The object pointer
480             @param cr: the current row, from the database cursor,
481             @param uid: the current user’s ID for security checks,
482             @param data_id: Get Data’s ID or False
483             @param context: A standard dictionary for contextual values
484         """
485
486         if not context:
487             context = {}
488         vals = []
489         ical_data = base64.decodestring(content)
490         parsedCal = vobject.readOne(ical_data)
491         if not data_id:
492             data_id = self.search(cr, uid, [])[0]
493         cal = self.browse(cr, uid, data_id)
494         cal_children = {}
495         count = 0
496         for line in cal.line_ids:
497             cal_children[line.name] = line.object_id.model
498         for child in parsedCal.getChildren():
499             if child.name.lower() in cal_children:
500                 context.update({'model': cal_children[child.name.lower()],
501                                 'calendar_id': cal.id
502                                 })
503                 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
504                 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
505                 vals.append(val)
506                 obj = self.pool.get(cal_children[child.name.lower()])
507         if hasattr(obj, 'check_import'):
508             obj.check_import(cr, uid, vals, context=context)
509         else:
510             self.check_import(cr, uid, vals, context=context)
511         return {}
512 Calendar()
513
514
515 class basic_calendar_line(osv.osv):
516     """ Calendar Lines """
517
518     _name = 'basic.calendar.lines'
519     _description = 'Calendar Lines'
520
521     _columns = {
522             'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
523                                     ('valarm', 'Alarm'), \
524                                     ('attendee', 'Attendee')], \
525                                     string="Type", size=64),
526             'object_id': fields.many2one('ir.model', 'Object'),
527             'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
528                                        required=True, ondelete='cascade'),
529             'domain': fields.char('Domain', size=124),
530             'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
531     }
532
533     _defaults = {
534         'domain': lambda *a: '[]',
535     }
536
537     def create(self, cr, uid, vals, context={}):
538         """ create calendar's line
539             @param self: The object pointer
540             @param cr: the current row, from the database cursor,
541             @param uid: the current user’s ID for security checks,
542             @param vals: Get the Values
543             @param context: A standard dictionary for contextual values
544         """
545
546         cr.execute("Select count(id) from basic_calendar_lines \
547                                 where name='%s' and calendar_id=%s" % (vals.get('name'), vals.get('calendar_id')))
548         res = cr.fetchone()
549         if res:
550             if res[0] > 0:
551                 raise osv.except_osv(_('Warning !'), _('Can not create \
552 line "%s" more than once' % (vals.get('name'))))
553         return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
554
555 basic_calendar_line()
556
557
558 class basic_calendar_attribute(osv.osv):
559     _name = 'basic.calendar.attributes'
560     _description = 'Calendar attributes'
561     _columns = {
562         'name': fields.char("Name", size=64, required=True),
563         'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
564                                     ('alarm', 'Alarm'), \
565                                     ('attendee', 'Attendee')], \
566                                     string="Type", size=64, required=True),
567     }
568
569 basic_calendar_attribute()
570
571
572 class basic_calendar_fields(osv.osv):
573     """ Calendar fields """
574
575     _name = 'basic.calendar.fields'
576     _description = 'Calendar fields'
577
578     _columns = {
579         'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
580         'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
581         'type_id': fields.many2one('basic.calendar.lines', 'Type', \
582                                    required=True, ondelete='cascade'),
583         'expr': fields.char("Expression", size=64),
584         'fn': fields.selection([('field', 'Use the field'),
585                         ('const', 'Expression as constant'),
586                         ('hours', 'Interval in hours'),
587                         ], 'Function'),
588         'mapping': fields.text('Mapping'),
589     }
590
591     _defaults = {
592         'fn': lambda *a: 'field',
593     }
594
595     def check_line(self, cr, uid, vals, name, context=None):
596         """ check calendar's line
597             @param self: The object pointer
598             @param cr: the current row, from the database cursor,
599             @param uid: the current user’s ID for security checks,
600             @param vals: Get Values
601             @param context: A standard dictionary for contextual values
602         """
603         f_obj = self.pool.get('ir.model.fields')
604         field = f_obj.browse(cr, uid, vals['field_id'], context=context)
605         relation = field.relation
606         line_obj = self.pool.get('basic.calendar.lines')
607         l_id = line_obj.search(cr, uid, [('name', '=', name)])
608         if l_id:
609             line = line_obj.browse(cr, uid, l_id, context=context)[0]
610             line_rel = line.object_id.model
611             if (relation != 'NULL') and (not relation == line_rel):
612                 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name)))
613         return True
614
615     def create(self, cr, uid, vals, context={}):
616         """ Create Calendar's fields
617             @param self: The object pointer
618             @param cr: the current row, from the database cursor,
619             @param uid: the current user’s ID for security checks,
620             @param vals: Get Values
621             @param context: A standard dictionary for contextual values
622         """
623
624         cr.execute('select name from basic_calendar_attributes \
625                             where id=%s' % (vals.get('name')))
626         name = cr.fetchone()
627         name = name[0]
628         if name in ('valarm', 'attendee'):
629             self.check_line(cr, uid, vals, name, context=context)
630         cr.execute("Select count(id) from basic_calendar_fields \
631                                 where name=%s and type_id=%s" % (vals.get('name'), vals.get('type_id')))
632         res = cr.fetchone()
633         if res:
634             if res[0] > 0:
635                 raise osv.except_osv(_('Warning !'), _('Can not map the field more than once'))
636         return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
637
638     def write(self, cr, uid, ids, vals, context=None):
639         """ write Calendar's fields
640             @param self: The object pointer
641             @param cr: the current row, from the database cursor,
642             @param uid: the current user’s ID for security checks,
643             @param vals: Get Values
644             @param context: A standard dictionary for contextual values
645         """
646
647         if not vals:
648             return
649         for id in ids:
650             field = self.browse(cr, uid, id, context=context)
651             name = field.name.name
652             if name in ('valarm', 'attendee'):
653                 self.check_line(cr, uid, vals, name, context=context)
654             qry = "Select count(id) from basic_calendar_fields \
655                                 where name=%s and type_id=%s" % (field.name.id, field.type_id.id)
656             cr.execute(qry)
657             res = cr.fetchone()
658             if res:
659                 if res[0] > 1:
660                     raise osv.except_osv(_('Warning !'), _('Can not map same field more than once'))
661         return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
662
663 basic_calendar_fields()
664
665
666 class Event(CalDAV, osv.osv_memory):
667     _name = 'basic.calendar.event'
668     _calname = 'vevent'
669     __attribute__ = {
670         'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar  component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
671         '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.
672         'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
673         'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
674         'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
675         '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.
676         'location': None, # Use: O-1, Type: TEXT            Defines the intended venue for the activity defined by a calendar component.
677         'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
678         'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
679         'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
680         'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
681         'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
682         'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
683         'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
684         'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
685         'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
686         'recurid': None,
687         'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
688         'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
689         'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
690         'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
691         'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a  reference to contact information associated with the calendar component.
692         'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
693         'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
694         'rstatus': None,
695         'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
696                                 #  like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
697         '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
698         'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
699         'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
700         'x-prop': None,
701         'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
702         'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
703     }
704
705     def export_cal(self, cr, uid, datas, vobj='vevent', context={}):
706         """ Export calendar
707             @param self: The object pointer
708             @param cr: the current row, from the database cursor,
709             @param uid: the current user’s ID for security checks,
710             @param datas: Get datas
711             @param context: A standard dictionary for contextual values
712         """
713
714         return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
715
716 Event()
717
718
719 class ToDo(CalDAV, osv.osv_memory):
720     _name = 'basic.calendar.todo'
721     _calname = 'vtodo'
722
723     __attribute__ = {
724                 'class': None,
725                 'completed': None,
726                 'created': None,
727                 'description': None,
728                 'dtstamp': None,
729                 'dtstart': None,
730                 'duration': None,
731                 'due': None,
732                 'geo': None,
733                 'last-mod ': None,
734                 'location': None,
735                 'organizer': None,
736                 'percent': None,
737                 'priority': None,
738                 'recurid': None,
739                 'seq': None,
740                 'status': None,
741                 'summary': None,
742                 'uid': None,
743                 'url': None,
744                 'attach': None,
745                 'attendee': None,
746                 'categories': None,
747                 'comment': None,
748                 'contact': None,
749                 'exdate': None,
750                 'exrule': None,
751                 'rstatus': None,
752                 'related': None,
753                 'resources': None,
754                 'rdate': None,
755                 'rrule': None,
756             }
757
758     def export_cal(self, cr, uid, datas, vobj='vevent', context={}):
759         """ Export Calendar
760             @param self: The object pointer
761             @param cr: the current row, from the database cursor,
762             @param uid: the current user’s ID for security checks,
763             @param datas: Get datas
764             @param context: A standard dictionary for contextual values
765         """
766
767         return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
768
769 ToDo()
770
771
772 class Journal(CalDAV):
773     __attribute__ = {
774     }
775
776
777 class FreeBusy(CalDAV):
778     __attribute__ = {
779     'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a  reference to contact information associated with the calendar component.
780     'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
781     'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
782     'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
783     'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
784     'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
785     'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
786     'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
787     'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
788     'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
789     'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
790     'rstatus': None,
791     'X-prop': None,
792     }
793
794
795 class Timezone(CalDAV, osv.osv_memory):
796     _name = 'basic.calendar.timezone'
797     _calname = 'vtimezone'
798
799     __attribute__ = {
800     'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
801     '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.
802     '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.
803     'standardc': {'tzprop': None}, # Use: R-1,
804     'daylightc': {'tzprop': None}, # Use: R-1,
805     'x-prop': None, # Use: O-n, Type: Text,
806     }
807
808     def get_name_offset(self, cr, uid, tzid, context={}):
809         """ Get Name Offset value
810             @param self: The object pointer
811             @param cr: the current row, from the database cursor,
812             @param uid: the current user’s ID for security checks,
813             @param context: A standard dictionary for contextual values
814         """
815
816         mytz = pytz.timezone(tzid.title())
817         mydt = datetime.now(tz=mytz)
818         offset = mydt.utcoffset()
819         val = offset.days * 24 + float(offset.seconds) / 3600
820         realoffset = '%02d%02d' % (math.floor(abs(val)), \
821                                  round(abs(val) % 1 + 0.01, 2) * 60)
822         realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
823         return (mydt.tzname(), realoffset)
824
825     def export_cal(self, cr, uid, model, tzid, ical, context={}):
826         """ Export Calendar
827             @param self: The object pointer
828             @param cr: the current row, from the database cursor,
829             @param uid: the current user’s ID for security checks,
830             @param model: Get Model's name
831             @param context: A standard dictionary for contextual values
832         """
833
834         ctx = context.copy()
835         ctx.update({'model': model})
836         cal_tz = ical.add('vtimezone')
837         cal_tz.add('TZID').value = tzid.title()
838         tz_std = cal_tz.add('STANDARD')
839         tzname, offset = self.get_name_offset(cr, uid, tzid)
840         tz_std.add("TZOFFSETFROM").value = offset
841         tz_std.add("TZOFFSETTO").value = offset
842         tz_std.add("DTSTART").value = datetime.now() # TODO
843         tz_std.add("TZNAME").value = tzname
844         return ical
845
846     def import_cal(self, cr, uid, ical_data, context=None):
847         """ Import Calendar
848             @param self: The object pointer
849             @param cr: the current row, from the database cursor,
850             @param uid: the current user’s ID for security checks,
851             @param ical_data: Get calendar's data
852             @param context: A standard dictionary for contextual values
853         """
854
855         for child in ical_data.getChildren():
856             if child.name.lower() == 'tzid':
857                 tzname = child.value
858                 self.ical_set(child.name.lower(), tzname, 'value')
859         vals = map_data(cr, uid, self)
860         return vals
861
862 Timezone()
863
864
865 class Alarm(CalDAV, osv.osv_memory):
866     _name = 'basic.calendar.alarm'
867     _calname = 'alarm'
868
869     __attribute__ = {
870     'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
871     '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
872     'summary': None, # Use: R-1, Type: Text        Which contains the text to be used as the message subject. Use for EMAIL
873     '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
874     '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
875     '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
876     '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
877     '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.
878     'x-prop': None,
879     }
880
881     def export_cal(self, cr, uid, model, alarm_id, vevent, context={}):
882         """ Export Calendar
883             @param self: The object pointer
884             @param cr: the current row, from the database cursor,
885             @param uid: the current user’s ID for security checks,
886             @param model: Get Model's name
887             @param alarm_id: Get Alarm's Id
888             @param context: A standard dictionary for contextual values
889         """
890
891         valarm = vevent.add('valarm')
892         alarm_object = self.pool.get(model)
893         alarm_data = alarm_object.read(cr, uid, alarm_id, [])
894
895         # Compute trigger data
896         interval = alarm_data['trigger_interval']
897         occurs = alarm_data['trigger_occurs']
898         duration = (occurs == 'after' and alarm_data['trigger_duration']) \
899                                         or -(alarm_data['trigger_duration'])
900         related = alarm_data['trigger_related']
901         trigger = valarm.add('TRIGGER')
902         trigger.params['related'] = [related.upper()]
903         if interval == 'days':
904             delta = timedelta(days=duration)
905         if interval == 'hours':
906             delta = timedelta(hours=duration)
907         if interval == 'minutes':
908             delta = timedelta(minutes=duration)
909         trigger.value = delta
910
911         # Compute other details
912         valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
913         valarm.add('ACTION').value = alarm_data['action']
914         return vevent
915
916     def import_cal(self, cr, uid, ical_data, context=None):
917         """ Import Calendar
918             @param self: The object pointer
919             @param cr: the current row, from the database cursor,
920             @param uid: the current user’s ID for security checks,
921             @param ical_data: Get calendar's Data
922             @param context: A standard dictionary for contextual values
923         """
924
925         ctx = context.copy()
926         ctx.update({'model': context.get('model', None)})
927         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
928         for child in ical_data.getChildren():
929             if child.name.lower() == 'trigger':
930                 seconds = child.value.seconds
931                 days = child.value.days
932                 diff = (days * 86400) +  seconds
933                 interval = 'days'
934                 related = 'before'
935                 if not seconds:
936                     duration = abs(days)
937                     related = days > 0 and 'after' or 'before'
938                 elif (abs(diff) / 3600) == 0:
939                     duration = abs(diff / 60)
940                     interval = 'minutes'
941                     related = days >= 0 and 'after' or 'before'
942                 else:
943                     duration = abs(diff / 3600)
944                     interval = 'hours'
945                     related = days >= 0 and 'after' or 'before'
946                 self.ical_set('trigger_interval', interval, 'value')
947                 self.ical_set('trigger_duration', duration, 'value')
948                 self.ical_set('trigger_occurs', related.lower(), 'value')
949                 if child.params:
950                     if child.params.get('related'):
951                         self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
952             else:
953                 self.ical_set(child.name.lower(), child.value.lower(), 'value')
954         vals = map_data(cr, uid, self)
955         return vals
956
957 Alarm()
958
959
960 class Attendee(CalDAV, osv.osv_memory):
961     _name = 'basic.calendar.attendee'
962     _calname = 'attendee'
963
964     __attribute__ = {
965     'cutype': None, # Use: 0-1    Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
966     'member': None, # Use: 0-1    Specify the group or list membership of the calendar user specified by the property.
967     '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"
968     '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".
969     '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.
970     'delegated-to': None, # Use: 0-1    Specify the calendar users to whom the calendar user specified by the property has delegated participation.
971     'delegated-from': None, # Use: 0-1    Specify the calendar users that have delegated their participation to the calendar user specified by the property.
972     'sent-by': None, # Use: 0-1    Specify the calendar user that is acting on behalf of the calendar user specified by the property.
973     'cn': None, # Use: 0-1    Specify the common name to be associated with the calendar user specified by the property.
974     'dir': None, # Use: 0-1    Specify reference to a directory entry associated with the calendar user specified by the property.
975     'language': None, # Use: 0-1    Specify the language for text values in a property or property parameter.
976     }
977
978     def import_cal(self, cr, uid, ical_data, context=None):
979         """ Import Calendar
980             @param self: The object pointer
981             @param cr: the current row, from the database cursor,
982             @param uid: the current user’s ID for security checks,
983             @param ical_data: Get calendar's Data
984             @param context: A standard dictionary for contextual values
985         """
986
987         ctx = context.copy()
988         ctx.update({'model': context.get('model', None)})
989         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
990         for para in ical_data.params:
991             if para.lower() == 'cn':
992                 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
993                         ical_data.value, 'value')
994             else:
995                 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
996         if not ical_data.params.get('CN'):
997             self.ical_set('cn', ical_data.value, 'value')
998         vals = map_data(cr, uid, self)
999         return vals
1000
1001     def export_cal(self, cr, uid, model, attendee_ids, vevent, context={}):
1002         """ Export Calendar
1003             @param self: The object pointer
1004             @param cr: the current row, from the database cursor,
1005             @param uid: the current user’s ID for security checks,
1006             @param model: Get model's name
1007             @param attendee_ids: Get Attendee's Id
1008             @param context: A standard dictionary for contextual values
1009         """
1010
1011         attendee_object = self.pool.get(model)
1012         ctx = context.copy()
1013         ctx.update({'model': model})
1014         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1015         for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1016             attendee_add = vevent.add('attendee')
1017             cn_val = ''
1018             for a_key, a_val in self.__attribute__.items():
1019                 if attendee[a_val['field']] and a_val['field'] != 'cn':
1020                     if a_val['type'] in ('text', 'char', 'selection'):
1021                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1022                     elif a_val['type'] == 'boolean':
1023                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1024                 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1025                     cn_val = [str(attendee[a_val['field']])]
1026                     if cn_val:
1027                         attendee_add.params['CN'] = cn_val
1028             if not attendee['email']:
1029                 raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1030             elif attendee['email']:
1031                 attendee_add.value = 'MAILTO:' + attendee['email']
1032         return vevent
1033
1034 Attendee()
1035
1036 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: