From 76b724294aa21c83edd3409d4664e041dd70cd00 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Sat, 4 Mar 2017 22:45:53 +0100 Subject: [PATCH] [FIX] calendar: correct search in recurrent event Add a 3 use cases with tests that were failing before. datetime filters were not working correctly on stop filters The `_get_recurrent_date_by_event` was only computing recurrency on the start field of an event which can make a difference. A recurring event start=2017-01-01 09:00:00; stop=2017-01-01 10:00:0 was not matched in a search ('stop', '>', '2017-01-01 09:30:00') The filter != was not handled in the recurring use cases. e.g. making the search 'start is set' in the web client is equal to ('start', '!=', False) Searching with only a start filter matching recurring events but not the first ("real") occurence, was returning no result if no stop filter was present. start=2017-01-01 09:00:00; count=5 had no result in a search ('start', '>', '2017-01-02') --- addons/calendar/models/calendar.py | 67 ++++++++++++++----- .../test_calendar_recurrent_event_case2.py | 49 ++++++++++++++ 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 7db83a3cb29..55f99a5239b 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -523,9 +523,23 @@ class Meeting(models.Model): return partners @api.multi - def _get_recurrent_date_by_event(self): - """ Get recurrent dates based on Rule string and all event where recurrent_id is child """ + def _get_recurrent_dates_by_event(self): + """ Get recurrent start and stop dates based on Rule string""" + start_dates = self._get_recurrent_date_by_event(date_field='start') + stop_dates = self._get_recurrent_date_by_event(date_field='stop') + return zip(start_dates, stop_dates) + + @api.multi + def _get_recurrent_date_by_event(self, date_field='start'): + """ Get recurrent dates based on Rule string and all event where recurrent_id is child + + date_field: the field containing the reference date information for recurrency computation + """ self.ensure_one() + if date_field in self._fields.keys() and self._fields[date_field].type in ('date', 'datetime'): + reference_date = self[date_field] + else: + reference_date = self.start def todate(date): val = parser.parse(''.join((re.compile('\d')).findall(date))) @@ -535,14 +549,14 @@ class Meeting(models.Model): return val.astimezone(timezone) timezone = pytz.timezone(self._context.get('tz') or 'UTC') - startdate = pytz.UTC.localize(fields.Datetime.from_string(self.start)) # Add "+hh:mm" timezone - if not startdate: - startdate = datetime.now() + event_date = pytz.UTC.localize(fields.Datetime.from_string(reference_date)) # Add "+hh:mm" timezone + if not event_date: + event_date = datetime.now() - # Convert the start date to saved timezone (or context tz) as it'll + # Convert the event 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) # transform "+hh:mm" timezone - rset1 = rrule.rrulestr(str(self.rrule), dtstart=startdate, forceset=True) + event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone + rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True) recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)]) for meeting in recurring_meetings: @@ -986,27 +1000,38 @@ class Meeting(models.Model): result.append(meeting.id) result_data.append(meeting.get_search_fields(order_fields)) continue - rdates = meeting._get_recurrent_date_by_event() + rdates = meeting._get_recurrent_dates_by_event() - for r_date in rdates: + for r_start_date, r_stop_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 + r_date = r_start_date # default for empty domain for arg in domain: if str(arg[0]) in ('start', 'stop', 'final_date'): + if str(arg[0]) == 'start': + r_date = r_start_date + else: + r_date = r_stop_date + if arg[2] and len(arg[2]) > len(r_date.strftime(DEFAULT_SERVER_DATE_FORMAT)): + dformat = DEFAULT_SERVER_DATETIME_FORMAT + else: + dformat = DEFAULT_SERVER_DATE_FORMAT if (arg[1] == '='): - ok = r_date.strftime('%Y-%m-%d') == arg[2] + ok = r_date.strftime(dformat) == arg[2] if (arg[1] == '>'): - ok = r_date.strftime('%Y-%m-%d') > arg[2] + ok = r_date.strftime(dformat) > arg[2] if (arg[1] == '<'): - ok = r_date.strftime('%Y-%m-%d') < arg[2] + ok = r_date.strftime(dformat) < arg[2] if (arg[1] == '>='): - ok = r_date.strftime('%Y-%m-%d') >= arg[2] + ok = r_date.strftime(dformat) >= arg[2] if (arg[1] == '<='): - ok = r_date.strftime('%Y-%m-%d') <= arg[2] + ok = r_date.strftime(dformat) <= arg[2] + if (arg[1] == '!='): + ok = r_date.strftime(dformat) != arg[2] pile.append(ok) elif str(arg) == str('&') or str(arg) == str('|'): pile.append(arg) @@ -1531,6 +1556,18 @@ class Meeting(models.Model): if not self._context.get('virtual_id', True): return super(Meeting, self).search(new_args, offset=offset, limit=limit, order=order, count=count) + if any(arg[0] == 'start' for arg in args) and \ + not any(arg[0] in ('stop', 'final_date') for arg in args): + # domain with a start filter but with no stop clause should be extended + # e.g. start=2017-01-01, count=5 => virtual occurences must be included in ('start', '>', '2017-01-02') + start_args = new_args + new_args = [] + for arg in start_args: + new_arg = arg + if arg[0] in ('start_date', 'start_datetime', 'start',): + new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])] + new_args.append(new_arg) + # offset, limit, order and count must be treated separately as we may need to deal with virtual ids events = super(Meeting, self).search(new_args, offset=0, limit=0, order=None, count=False) events = self.browse(events.get_recurrent_ids(args, order=order)) diff --git a/addons/calendar/tests/test_calendar_recurrent_event_case2.py b/addons/calendar/tests/test_calendar_recurrent_event_case2.py index da39fa6509b..e05428593ae 100644 --- a/addons/calendar/tests/test_calendar_recurrent_event_case2.py +++ b/addons/calendar/tests/test_calendar_recurrent_event_case2.py @@ -96,3 +96,52 @@ class TestRecurrentEvent(common.TransactionCase): # I update the description of two meetings, and check that both have been updated self.calendar_event_sprint_review.write({'description': "Some description"}) self.assertEqual(self.calendar_event_sprint_review.description, "Some description", "Event %d has not been updated" % self.calendar_event_sprint_review.id) + + def test_recurrent_meeting4(self): + # I create a weekly meeting till a particular end date. + self.CalendarEvent.create({ + 'start': '2017-01-22 11:47:00', + 'stop': '2017-01-22 12:47:00', + 'day': 0.0, + 'duration': 1.0, + 'final_date': '2017-06-30', + 'end_type': 'end_date', + 'fr': True, + 'mo': True, + 'th': True, + 'tu': True, + 'we': True, + 'name': 'Review code with programmer', + 'recurrency': True, + 'rrule_type': 'weekly' + }) + + # I search for a recurrent weekly meetings that take place at a given date. + meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([ + ('start', '<=', '2017-01-24'), ('stop', '>=', '2017-01-24'), ('name', '=', 'Review code with programmer') + ]) + self.assertEqual(meetings_count, 1, 'Recurrent weekly meetings are not found using date filter !') + + # I search for a recurrent weekly meetings that take place at a given date and time. + meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([ + ('start', '<=', '2017-01-24 11:55:00'), ('stop', '>=', '2017-01-24 11:55:00'), ('name', '=', 'Review code with programmer') + ]) + self.assertEqual(meetings_count, 1, 'Recurrent weekly meetings are not found using time filter !') + + # I search using the filter 'start date is set' + meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([ + ('start', '!=', False), ('stop', '>=', '2017-06-30 11:55:00'), ('name', '=', 'Review code with programmer') + ]) + self.assertEqual(meetings_count, 1, "Last recurrent weekly meetings are not found using 'is set' filter !") + + # I search for a recurrent weekly meetings that take place at a given date and time. + meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([ + ('start', '<=', '2017-01-24 11:55:00'), ('stop', '>=', '2017-01-24 15:55:00') + ]) + self.assertEqual(meetings_count, 0, 'Too late recurrent meetings are found using time filter !') + + # I search using a start filter but no stop + meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([ + ('start', '>=', '2017-06-30 08:00:00'), ('name', '=', 'Review code with programmer') + ]) + self.assertEqual(meetings_count, 1, "Last recurrent weekly meetings are not found without stop filter !")