diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 397bd193757..b823ed5439c 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -38,17 +38,41 @@ months = { 10: "October", 11: "November", 12: "December" } -def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None): - """ - Get recurrent dates based on Rule string considering exdate and start date. - @param rrulestring: rulestring - @param exdate: list of exception dates for rrule - @param startdate: startdate for computing recurrent dates + +def get_recurrent_dates(rrulestring, startdate, exdate=None, tz=None, exrule=None, context=None): + """Get recurrent dates based on Rule string considering exdate and start date. + + All input dates and output dates are in UTC. Dates are infered + thanks to rules in the ``tz`` timezone if given, else it'll be in + the current local timezone as specified in the context. + + @param rrulestring: rulestring (ie: 'FREQ=DAILY;INTERVAL=1;COUNT=3') + @param exdate: string of dates separated by commas (ie: '20130506220000Z,20130507220000Z') + @param startdate: string start date for computing recurrent dates + @param tz: pytz timezone for computing recurrent dates + @param exrule: string exrule + @param context: current openerp context (for local timezone if ``tz`` is not provided) @return: list of Recurrent dates + """ + + exdate = exdate.split(',') if exdate else [] + startdate = pytz.UTC.localize( + datetime.strptime(startdate, "%Y-%m-%d %H:%M:%S")) + def todate(date): val = parser.parse(''.join((re.compile('\d')).findall(date))) - return val + ## Dates are localized to saved timezone if any, else defaulted to + ## current timezone. WARNING: these last event dates are considered as + ## "floating" dates. + if not val.tzinfo: + val = pytz.UTC.localize(val) + return val.astimezone(timezone) + + ## Note that we haven't any context tz info when called by the server, so + ## we'll default to UTC which could induce one-day errors in date + ## calculation. + timezone = pytz.timezone(tz or context.get('tz') or 'UTC') if not startdate: startdate = datetime.now() @@ -56,6 +80,9 @@ def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None): if not exdate: exdate = [] + ## Convert the start date to saved timezone (or context tz) as it'll + ## define the correct hour/day asked by the user to repeat for recurrence. + startdate = startdate.astimezone(timezone) rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True) for date in exdate: datetime_obj = todate(date) @@ -64,7 +91,9 @@ def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None): if exrule: rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate)) - return list(rset1) + return [d.astimezone(pytz.UTC) for d in rset1] + + def base_calendar_id2real_id(base_calendar_id=None, with_date=False): """ @@ -821,10 +850,7 @@ class calendar_alarm(osv.osv): re_dates = [] if hasattr(res_obj, 'rrule') and res_obj.rrule: - event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S') - #exdate is a string and we need a list - exdate = res_obj.exdate and res_obj.exdate.split(',') or [] - recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule) + recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.date, res_obj.exdate, res_obj.vtimezone, res_obj.exrule, context=context) trigger_interval = alarm.trigger_interval if trigger_interval == 'days': @@ -986,6 +1012,47 @@ class calendar_event(osv.osv): result[event] = "" return result + + def _get_recurrence_end_date(self, cr, uid, ids, name, arg, context=None): + """Get a good estimate of the end of the timespan concerned by an event. + + This means we need to concider the last event of a recurrency, and that we + add its duration. For simple events (no rrule), the date_deadline is sufficient. + + This value is stored in database and will help select events that should be + concidered candidate for display when filters are made upon dates (typically + the agenda filter will make one-month, one-week, one-day timespan searches). + + """ + + if not context: + context = {} + events = super(calendar_event, self).read( + cr, uid, ids, ['rrule', 'exdate', 'exrule', 'duration', 'date_deadline', 'date', 'vtimezone'], context=context) + + result = {} + for event in events: + + duration = timedelta(hours=event['duration']) + + if event['rrule']: + all_dates = get_recurrent_dates( + event['rrule'], event['date'], event['exdate'], event['vtimezone'], + event['exrule'], context=context) + if not event['vtimezone'] and not context.get('tz'): + ## We are called by the server probably at update time (no + ## context), and no vtimezone was recorded, so we have no + ## idea of possible client timezone so we have a possible + ## one-day-of error when applying RRULEs on floating dates. + ## Let's add a day. + duration += timedelta(days=1) + result[event['id']] = (all_dates[-1] + duration).astimezone(pytz.UTC).strftime("%Y-%m-%d %H:%M:%S") \ + if all_dates else None + else: + result[event['id']] = event['date_deadline'] + + return result + def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None): data = self._get_empty_rrule_data() if field_value: @@ -1035,6 +1102,10 @@ rule or repeating pattern of time to exclude from the recurring rule."), 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'), 'recurrent_id': fields.integer('Recurrent ID'), 'recurrent_id_date': fields.datetime('Recurrent ID date'), + 'recurrence_end_date': fields.function(_get_recurrence_end_date, + type='datetime', + store=True, string='Recurrence end date', + priority=30), 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'), 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}), 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent. @@ -1154,39 +1225,43 @@ rule or repeating pattern of time to exclude from the recurring rule."), context = {} result = [] - for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'recurrency', 'exdate', 'exrule', 'date'], context=context): +# for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'recurrency', 'exdate', 'exrule', 'date'], context=context): + for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'recurrency', 'exdate', 'exrule', 'date', 'vtimezone'], context=context): if not data['recurrency'] or not data['rrule']: result.append(data['id']) continue - event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S") - +# event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S") +# event_date = pytz.UTC.localize(event_date) # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code - if not data['rrule']: - continue - - exdate = data['exdate'] and data['exdate'].split(',') or [] - rrule_str = data['rrule'] - new_rrule_str = [] - rrule_until_date = False - is_until = False - for rule in rrule_str.split(';'): - name, value = rule.split('=') - if name == "UNTIL": - is_until = True - value = parser.parse(value) - rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S")) - value = value.strftime("%Y%m%d%H%M%S") - new_rule = '%s=%s' % (name, value) - new_rrule_str.append(new_rule) - new_rrule_str = ';'.join(new_rrule_str) - rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule']) +# if not data['rrule']: +# continue +# +# exdate = data['exdate'] and data['exdate'].split(',') or [] +# rrule_str = data['rrule'] +# new_rrule_str = [] +# rrule_until_date = False +# is_until = False +# for rule in rrule_str.split(';'): +# name, value = rule.split('=') +# if name == "UNTIL": +# is_until = True +# value = parser.parse(value) +# rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S")) +# value = value.strftime("%Y%m%d%H%M%S") +# new_rule = '%s=%s' % (name, value) +# new_rrule_str.append(new_rule) +# new_rrule_str = ';'.join(new_rrule_str) +# rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule']) +# rdates = get_recurrent_dates(data['rrule'], exdate, event_date, data['exrule']) + rdates = get_recurrent_dates(data['rrule'], data['date'], data['exdate'], data['vtimezone'], data['exrule'], context=context) for r_date in rdates: # fix domain evaluation # step 1: check date and replace expression by True or False, replace other expressions by True # step 2: evaluation of & and | # check if there are one False pile = [] + ok = True for arg in domain: if str(arg[0]) in (str('date'), str('date_deadline')): if (arg[1] == '='): @@ -1346,7 +1421,8 @@ rule or repeating pattern of time to exclude from the recurring rule."), new_arg = arg if arg[0] in ('date_deadline', unicode('date_deadline')): if context.get('virtual_id', True): - new_args += ['|','&',('recurrency','=',1),('end_date', arg[1], arg[2])] +# new_args += ['|','&',('recurrency','=',1),('end_date', arg[1], arg[2])] + new_args += ['|','&',('recurrency','=',1),('recurrence_end_date', arg[1], arg[2])] elif arg[0] == "id": new_id = get_real_ids(arg[2]) new_arg = (arg[0], arg[1], new_id) @@ -1421,7 +1497,7 @@ rule or repeating pattern of time to exclude from the recurring rule."), new_id = self.copy(cr, uid, real_event_id, default=data, context=context) date_new = event_id.split('-')[1] - date_new = time.strftime("%Y%m%dT%H%M%S", \ + date_new = time.strftime("%Y%m%dT%H%M%SZ", \ time.strptime(date_new, "%Y%m%d%H%M%S")) exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new res = self.write(cr, uid, [real_event_id], {'exdate': exdate}) @@ -1472,7 +1548,8 @@ rule or repeating pattern of time to exclude from the recurring rule."), context = {} fields2 = fields and fields[:] or None - EXTRAFIELDS = ('class','user_id','duration') + EXTRAFIELDS = ('class','user_id','duration', 'date', + 'rrule', 'vtimezone', 'exrule', 'exdate') for f in EXTRAFIELDS: if fields and (f not in fields): fields2.append(f) @@ -1495,6 +1572,15 @@ rule or repeating pattern of time to exclude from the recurring rule."), res = real_data[real_id].copy() ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0) if not isinstance(ls, (str, int, long)) and len(ls) >= 2: + recurrent_dates = [ + d.strftime("%Y-%m-%d %H:%M:%S") + for d in get_recurrent_dates( + res['rrule'], res['date'], res['exdate'], + res['vtimezone'], res['exrule'], context=context)] + if ls[1] not in recurrent_dates: + raise KeyError( + 'Virtual id %r is not valid, event %r can ' + 'not produce it.' % (base_calendar_id, real_id)) res['date'] = ls[1] res['date_deadline'] = ls[2] res['id'] = base_calendar_id @@ -1548,7 +1634,7 @@ rule or repeating pattern of time to exclude from the recurring rule."), date_new = time.strftime("%Y%m%dT%H%M%S", \ time.strptime(date_new, "%Y%m%d%H%M%S")) exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new - self.write(cr, uid, [real_event_id], {'exdate': exdate}) + self.write(cr, uid, [real_event_id], {'exdate': exdate}, context=context) ids.remove(event_id) for event in self.browse(cr, uid, ids, context=context): if event.attendee_ids: diff --git a/addons/base_calendar/base_calendar_view.xml b/addons/base_calendar/base_calendar_view.xml index 3ef2c8f59ce..f7f46565d63 100644 --- a/addons/base_calendar/base_calendar_view.xml +++ b/addons/base_calendar/base_calendar_view.xml @@ -239,7 +239,7 @@ calendar.event - +