Merge lp:~0k.io/openobject-addons/7.0-fix-base-calendar-bugs into lp:openobject-addons/7.0

Proposed by Nicolas JEUDY
Status: Needs review
Proposed branch: lp:~0k.io/openobject-addons/7.0-fix-base-calendar-bugs
Merge into: lp:openobject-addons/7.0
Diff against target: 248 lines (+101/-39)
2 files modified
base_calendar/base_calendar.py (+100/-38)
base_calendar/base_calendar_view.xml (+1/-1)
To merge this branch: bzr merge lp:~0k.io/openobject-addons/7.0-fix-base-calendar-bugs
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+174561@code.launchpad.net

Description of the change

This branch needs this other branch on openobject-server to work:

https://code.launchpad.net/~0k.io/openobject-server/7.0-use-priority-attribute-for-store-function/+merge/174560

In addition to correct both bugs that are linked to this branch, it'll:

1 - explicitely refuse to read virtual id that can't be generated thanks to a real event rrule.

  This is helpful to catch soon bugs as this one:

    - https://bugs.launchpad.net/openerp-web/+bug/1185823

2 - correct subtile bugs dangling around by storing timezone
  of when you create your recurring event rule.

  For instance, what to do if we want to store "all day"
  events when you are working in Hong-Kong. Then go back to
  Paris, then look at your events...

3 - actually make better (more efficient, and working) query
  to database to catch all recurring event that would be
  selected by filters "<" or ">" on dates.

4 - remove duplicate code, and actually use pre-existing and
  not used field "vtimezone"

Any comment are welcome. This needs a serious review.

To post a comment you must log in.

Unmerged revisions

9250. By Valentin Lab

new: refuse to answer to read requests on virtual ids that cannot be generated from the recurrence rule.

9249. By Valentin Lab

fix: recurrent days are now correctly filtered and thus displayed in agenda view.

9248. By Valentin Lab

new: added recurrence_end_date stored function to allow efficient filtering via SQL.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'base_calendar/base_calendar.py'
2--- base_calendar/base_calendar.py 2013-06-20 13:21:32 +0000
3+++ base_calendar/base_calendar.py 2013-07-13 16:13:06 +0000
4@@ -37,17 +37,41 @@
5 10: "October", 11: "November", 12: "December"
6 }
7
8-def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None):
9- """
10- Get recurrent dates based on Rule string considering exdate and start date.
11- @param rrulestring: rulestring
12- @param exdate: list of exception dates for rrule
13- @param startdate: startdate for computing recurrent dates
14+
15+def get_recurrent_dates(rrulestring, startdate, exdate=None, tz=None, exrule=None, context=None):
16+ """Get recurrent dates based on Rule string considering exdate and start date.
17+
18+ All input dates and output dates are in UTC. Dates are infered
19+ thanks to rules in the ``tz`` timezone if given, else it'll be in
20+ the current local timezone as specified in the context.
21+
22+ @param rrulestring: rulestring (ie: 'FREQ=DAILY;INTERVAL=1;COUNT=3')
23+ @param exdate: string of dates separated by commas (ie: '20130506220000Z,20130507220000Z')
24+ @param startdate: string start date for computing recurrent dates
25+ @param tz: pytz timezone for computing recurrent dates
26+ @param exrule: string exrule
27+ @param context: current openerp context (for local timezone if ``tz`` is not provided)
28 @return: list of Recurrent dates
29+
30 """
31+
32+ exdate = exdate.split(',') if exdate else []
33+ startdate = pytz.UTC.localize(
34+ datetime.strptime(startdate, "%Y-%m-%d %H:%M:%S"))
35+
36 def todate(date):
37 val = parser.parse(''.join((re.compile('\d')).findall(date)))
38- return val
39+ ## Dates are localized to saved timezone if any, else defaulted to
40+ ## current timezone. WARNING: these last event dates are considered as
41+ ## "floating" dates.
42+ if not val.tzinfo:
43+ val = pytz.UTC.localize(val)
44+ return val.astimezone(timezone)
45+
46+ ## Note that we haven't any context tz info when called by the server, so
47+ ## we'll default to UTC which could induce one-day errors in date
48+ ## calculation.
49+ timezone = pytz.timezone(tz or context.get('tz') or 'UTC')
50
51 if not startdate:
52 startdate = datetime.now()
53@@ -55,6 +79,9 @@
54 if not exdate:
55 exdate = []
56
57+ ## Convert the start date to saved timezone (or context tz) as it'll
58+ ## define the correct hour/day asked by the user to repeat for recurrence.
59+ startdate = startdate.astimezone(timezone)
60 rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True)
61 for date in exdate:
62 datetime_obj = todate(date)
63@@ -63,7 +90,9 @@
64 if exrule:
65 rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate))
66
67- return list(rset1)
68+ return [d.astimezone(pytz.UTC) for d in rset1]
69+
70+
71
72 def base_calendar_id2real_id(base_calendar_id=None, with_date=False):
73 """
74@@ -857,10 +886,7 @@
75 re_dates = []
76
77 if hasattr(res_obj, 'rrule') and res_obj.rrule:
78- event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
79- #exdate is a string and we need a list
80- exdate = res_obj.exdate and res_obj.exdate.split(',') or []
81- recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule)
82+ recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.date, res_obj.exdate, res_obj.vtimezone, res_obj.exrule, context=context)
83
84 trigger_interval = alarm.trigger_interval
85 if trigger_interval == 'days':
86@@ -1023,6 +1049,47 @@
87 result[event] = ""
88 return result
89
90+
91+ def _get_recurrence_end_date(self, cr, uid, ids, name, arg, context=None):
92+ """Get a good estimate of the end of the timespan concerned by an event.
93+
94+ This means we need to concider the last event of a recurrency, and that we
95+ add its duration. For simple events (no rrule), the date_deadline is sufficient.
96+
97+ This value is stored in database and will help select events that should be
98+ concidered candidate for display when filters are made upon dates (typically
99+ the agenda filter will make one-month, one-week, one-day timespan searches).
100+
101+ """
102+
103+ if not context:
104+ context = {}
105+ events = super(calendar_event, self).read(
106+ cr, uid, ids, ['rrule', 'exdate', 'exrule', 'duration', 'date_deadline', 'date', 'vtimezone'], context=context)
107+
108+ result = {}
109+ for event in events:
110+
111+ duration = timedelta(hours=event['duration'])
112+
113+ if event['rrule']:
114+ all_dates = get_recurrent_dates(
115+ event['rrule'], event['date'], event['exdate'], event['vtimezone'],
116+ event['exrule'], context=context)
117+ if not event['vtimezone'] and not context.get('tz'):
118+ ## We are called by the server probably at update time (no
119+ ## context), and no vtimezone was recorded, so we have no
120+ ## idea of possible client timezone so we have a possible
121+ ## one-day-of error when applying RRULEs on floating dates.
122+ ## Let's add a day.
123+ duration += timedelta(days=1)
124+ result[event['id']] = (all_dates[-1] + duration).astimezone(pytz.UTC).strftime("%Y-%m-%d %H:%M:%S") \
125+ if all_dates else None
126+ else:
127+ result[event['id']] = event['date_deadline']
128+
129+ return result
130+
131 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
132 data = self._get_empty_rrule_data()
133 if field_value:
134@@ -1072,6 +1139,10 @@
135 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
136 'recurrent_id': fields.integer('Recurrent ID'),
137 'recurrent_id_date': fields.datetime('Recurrent ID date'),
138+ 'recurrence_end_date': fields.function(_get_recurrence_end_date,
139+ type='datetime',
140+ store=True, string='Recurrence end date',
141+ priority=30),
142 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
143 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
144 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent.
145@@ -1189,34 +1260,15 @@
146 context = {}
147
148 result = []
149- for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'exdate', 'exrule', 'date'], context=context):
150+ for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'exdate', 'exrule', 'date', 'vtimezone'], context=context):
151 if not data['rrule']:
152 result.append(data['id'])
153 continue
154- event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
155-
156 # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code
157
158- if not data['rrule']:
159- continue
160-
161- exdate = data['exdate'] and data['exdate'].split(',') or []
162- rrule_str = data['rrule']
163- new_rrule_str = []
164- rrule_until_date = False
165- is_until = False
166- for rule in rrule_str.split(';'):
167- name, value = rule.split('=')
168- if name == "UNTIL":
169- is_until = True
170- value = parser.parse(value)
171- rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
172- value = value.strftime("%Y%m%d%H%M%S")
173- new_rule = '%s=%s' % (name, value)
174- new_rrule_str.append(new_rule)
175- new_rrule_str = ';'.join(new_rrule_str)
176- rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
177+ rdates = get_recurrent_dates(data['rrule'], data['date'], data['exdate'], data['vtimezone'], data['exrule'], context=context)
178 for r_date in rdates:
179+
180 ok = True
181 for arg in domain:
182 if arg[0] in ('date', 'date_deadline'):
183@@ -1357,7 +1409,7 @@
184 new_arg = arg
185 if arg[0] in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
186 if context.get('virtual_id', True):
187- new_args += ['|','&',('recurrency','=',1),('recurrent_id_date', arg[1], arg[2])]
188+ new_args += ['|','&',('recurrency','=',1),('recurrence_end_date', arg[1], arg[2])]
189 elif arg[0] == "id":
190 new_id = get_real_ids(arg[2])
191 new_arg = (arg[0], arg[1], new_id)
192@@ -1433,7 +1485,7 @@
193 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
194
195 date_new = event_id.split('-')[1]
196- date_new = time.strftime("%Y%m%dT%H%M%S", \
197+ date_new = time.strftime("%Y%m%dT%H%M%SZ", \
198 time.strptime(date_new, "%Y%m%d%H%M%S"))
199 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
200 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
201@@ -1476,7 +1528,8 @@
202 context = {}
203 fields2 = fields and fields[:] or None
204
205- EXTRAFIELDS = ('class','user_id','duration')
206+ EXTRAFIELDS = ('class','user_id','duration', 'date',
207+ 'rrule', 'vtimezone', 'exrule', 'exdate')
208 for f in EXTRAFIELDS:
209 if fields and (f not in fields):
210 fields2.append(f)
211@@ -1499,6 +1552,15 @@
212 res = real_data[real_id].copy()
213 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
214 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
215+ recurrent_dates = [
216+ d.strftime("%Y-%m-%d %H:%M:%S")
217+ for d in get_recurrent_dates(
218+ res['rrule'], res['date'], res['exdate'],
219+ res['vtimezone'], res['exrule'], context=context)]
220+ if ls[1] not in recurrent_dates:
221+ raise KeyError(
222+ 'Virtual id %r is not valid, event %r can '
223+ 'not produce it.' % (base_calendar_id, real_id))
224 res['date'] = ls[1]
225 res['date_deadline'] = ls[2]
226 res['id'] = base_calendar_id
227@@ -1552,7 +1614,7 @@
228 date_new = time.strftime("%Y%m%dT%H%M%S", \
229 time.strptime(date_new, "%Y%m%d%H%M%S"))
230 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
231- self.write(cr, uid, [real_event_id], {'exdate': exdate})
232+ self.write(cr, uid, [real_event_id], {'exdate': exdate}, context=context)
233 ids.remove(event_id)
234 for event in self.browse(cr, uid, ids, context=context):
235 if event.attendee_ids:
236
237=== modified file 'base_calendar/base_calendar_view.xml'
238--- base_calendar/base_calendar_view.xml 2013-02-25 13:38:04 +0000
239+++ base_calendar/base_calendar_view.xml 2013-07-13 16:13:06 +0000
240@@ -239,7 +239,7 @@
241 <field name="model">calendar.event</field>
242 <field name="priority" eval="2"/>
243 <field name="arch" type="xml">
244- <calendar string="Events" date_start="date" color="show_as" date_delay="duration">
245+ <calendar string="Events" date_start="date" color="show_as" date_delay="duration" all_day="allday">
246 <field name="name"/>
247 <field name="class"/>
248 <field name="show_as"/>