From 488e9f299deac6c0b0c7adc6fd68553ffa56e579 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 21 Jun 2016 12:01:43 +0200 Subject: [PATCH] [IMP] calendar: remove /calendar/notify RPC done every 5 minutes Basicaly, do this RPC once, at webclient startup, to get the next notification. Do the RPC again when the notification is displayed to get the next one. When an event with alarm is created, edited or removed, trigger an event on the bus for all involved users (the attendees) with an update of their next calendar notification. --- addons/calendar/calendar.py | 155 +++++++++++------- addons/calendar/controllers/__init__.py | 1 + addons/calendar/controllers/bus.py | 13 ++ .../calendar/static/src/js/base_calendar.js | 78 +++++---- 4 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 addons/calendar/controllers/bus.py diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index b738bea3bd4..8d25c9677b2 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -324,8 +324,17 @@ class res_partner(osv.Model): class calendar_alarm_manager(osv.AbstractModel): _name = 'calendar.alarm_manager' - def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None): + def get_next_potential_limit_alarm(self, cr, uid, alarm_type, seconds=None, partner_id=None, context=None): res = {} + delta_request = """ + SELECT + rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta + FROM + calendar_alarm_calendar_event_rel AS rel + LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id + WHERE alarm.type = %s + GROUP BY rel.calendar_event_id + """ base_request = """ SELECT cal.id, @@ -344,16 +353,7 @@ class calendar_alarm_manager(osv.AbstractModel): cal.rrule AS rule FROM calendar_event AS cal - RIGHT JOIN - ( - SELECT - rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta - FROM - calendar_alarm_calendar_event_rel AS rel - LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id - WHERE alarm.type in %s - GROUP BY rel.calendar_event_id - ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id + RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id """ filter_user = """ @@ -361,28 +361,36 @@ class calendar_alarm_manager(osv.AbstractModel): AND part_rel.res_partner_id = %s """ - #Add filter on type - type_to_read = () - if notif: - type_to_read += ('notification',) - if mail: - type_to_read += ('email',) + # Add filter on alarm type + tuple_params = (alarm_type,) - tuple_params = (type_to_read,) - - # ADD FILTER ON PARTNER_ID + # Add filter on partner_id if partner_id: base_request += filter_user tuple_params += (partner_id, ) - #Add filter on hours - tuple_params += (seconds,) + # Upper bound on first_alarm of requested events + first_alarm_max_value = "" + if seconds is None: + # first alarm in the future + 3 minutes if there is one, now otherwise + first_alarm_max_value = """ + COALESCE((SELECT MIN(cal.start - interval '1' minute * calcul_delta.max_delta) + FROM calendar_event cal + RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id + WHERE cal.start - interval '1' minute * calcul_delta.max_delta > now() at time zone 'utc' + ) + interval '3' minute, now() at time zone 'utc')""" + else: + # now + given seconds + first_alarm_max_value = "(now() at time zone 'utc' + interval '%s' second )" + tuple_params += (seconds,) - cr.execute("""SELECT * + cr.execute(""" + WITH calcul_delta AS (%s) + SELECT * FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS - WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second ) + WHERE ALL_EVENTS.first_alarm < %s AND ALL_EVENTS.last_event_date > (now() at time zone 'utc') - """ % base_request, tuple_params) + """ % (delta_request, base_request, first_alarm_max_value), tuple_params) for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall(): res[event_id] = { @@ -398,28 +406,20 @@ class calendar_alarm_manager(osv.AbstractModel): return res - def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, after=False, notif=True, mail=True, missing=False, context=None): + def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, alarm_type, after=False, missing=False, context=None): # one_date: date of the event to check (not the same that in the event browse if recurrent) # event: Event browse record # event_maxdelta: biggest duration from alarms for this event # in_the_next_X_seconds: looking in the future (in seconds) + # alarm_type: the type of alarm we are looking for ('notification' or 'email') # after: if not False: will return alert if after this date (date as string - todo: change in master) # missing: if not False: will return alert even if we are too late - # notif: Looking for type notification - # mail: looking for type email res = [] - - # TODO: replace notif and email in master by alarm_type + remove event_maxdelta and if using it - alarm_type = [] - if notif: - alarm_type.append('notification') - if mail: - alarm_type.append('email') - + # TODO: remove event_maxdelta and if using it if one_date - timedelta(minutes=(missing and 0 or event_maxdelta)) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date for alarm in event.alarm_ids: - if alarm.type in alarm_type and \ + if alarm.type == alarm_type and \ one_date - timedelta(minutes=(missing and 0 or alarm.duration_minutes)) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \ (not after or one_date - timedelta(minutes=alarm.duration_minutes) > openerp.fields.Datetime.from_string(after)): alert = { @@ -456,7 +456,7 @@ class calendar_alarm_manager(osv.AbstractModel): cron_interval = cron.interval_number * interval_to_second[cron.interval_type] - all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context) + all_events = self.get_next_potential_limit_alarm(cr, uid, 'email', seconds=cron_interval, context=context) for curEvent in self.pool.get('calendar.event').browse(cr, uid, all_events.keys(), context=context): max_delta = all_events[curEvent.id]['max_duration'] @@ -466,7 +466,7 @@ class calendar_alarm_manager(osv.AbstractModel): last_found = False for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context): in_date_format = one_date.replace(tzinfo=None) - last_found = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, 0, after=last_notif_mail, notif=False, missing=True, context=context) + last_found = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, 0, 'email', after=last_notif_mail, missing=True, context=context) for alert in last_found: self.do_mail_reminder(cr, uid, alert, context=context) at_least_one = True # if it's the first alarm for this recurrent event @@ -474,21 +474,20 @@ class calendar_alarm_manager(osv.AbstractModel): break else: in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT) - last_found = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, 0, after=last_notif_mail, notif=False, missing=True, context=context) + last_found = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, 0, 'email', after=last_notif_mail, missing=True, context=context) for alert in last_found: self.do_mail_reminder(cr, uid, alert, context=context) icp.set_param(cr, SUPERUSER_ID, 'calendar.last_notif_mail', now) def get_next_notif(self, cr, uid, context=None): - ajax_check_every_seconds = 300 partner = self.pool['res.users'].read(cr, SUPERUSER_ID, uid, ['partner_id', 'calendar_last_notif_ack'], context=context) all_notif = [] if not partner: return [] - all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner['partner_id'][0], mail=False, context=context) - + all_events = self.get_next_potential_limit_alarm(cr, uid, 'notification', partner_id=partner['partner_id'][0], context=context) + time_limit = 3600 * 24 # return alarms of the next 24 hours for event in all_events: # .values() max_delta = all_events[event]['max_duration'] curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context) @@ -497,7 +496,7 @@ class calendar_alarm_manager(osv.AbstractModel): LastFound = False for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context): in_date_format = one_date.replace(tzinfo=None) - LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner['calendar_last_notif_ack'], mail=False, context=context) + LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, time_limit, 'notification', after=partner['calendar_last_notif_ack'], context=context) if LastFound: for alert in LastFound: all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context)) @@ -507,7 +506,7 @@ class calendar_alarm_manager(osv.AbstractModel): break else: in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT) - LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner['calendar_last_notif_ack'], mail=False, context=context) + LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, time_limit, 'notification', after=partner['calendar_last_notif_ack'], context=context) if LastFound: for alert in LastFound: all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context)) @@ -536,9 +535,8 @@ class calendar_alarm_manager(osv.AbstractModel): def do_notif_reminder(self, cr, uid, alert, context=None): alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context) - event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context) - if alarm.type == 'notification': + event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context) message = event.display_time delta = alert['notify_at'] - datetime.now() @@ -552,6 +550,18 @@ class calendar_alarm_manager(osv.AbstractModel): 'notify_at': alert['notify_at'].strftime(DEFAULT_SERVER_DATETIME_FORMAT), } + def notify_next_alarm(self, cr, uid, partner_ids, context=None): + """ Sends through the bus the next alarm of given partners """ + notifications = [] + user_obj = self.pool['res.users'] + user_ids = user_obj.search(cr, uid, [('partner_id', 'in', tuple(partner_ids))], context=context) + users = user_obj.browse(cr, uid, user_ids, context=context) + for user in users: + notif = self.get_next_notif(cr, user.id, context=context) + notifications.append([(cr.dbname, 'calendar.alarm', user.partner_id.id), notif]) + if len(notifications) > 0: + self.pool['bus.bus'].sendmany(cr, uid, notifications) + class calendar_alarm(osv.Model): _name = 'calendar.alarm' @@ -1100,14 +1110,15 @@ class calendar_event(osv.Model): attendee_ids_to_remove = [] if partner_ids_to_remove: - attendee_ids_to_remove = self.pool["calendar.attendee"].search(cr, uid, [('partner_id.id', 'in', partner_ids_to_remove), ('event_id.id', '=', event.id)], context=context) + attendee_ids_to_remove = self.pool["calendar.attendee"].search(cr, uid, [('partner_id.id', 'in', partner_ids_to_remove), ('event_id', '=', event.id)], context=context) if attendee_ids_to_remove: self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context) res[event.id] = { 'new_attendee_ids': new_attendees, 'old_attendee_ids': all_attendee_ids, - 'removed_attendee_ids': attendee_ids_to_remove + 'removed_attendee_ids': attendee_ids_to_remove, + 'removed_partner_ids': partner_ids_to_remove } return res @@ -1566,6 +1577,7 @@ class calendar_event(osv.Model): ids = [ids] values0 = values + dont_notify_ctx = dict(context, dont_notify=True) # to prevent multiple notify_next_alarm # process events one by one for event_id in ids: @@ -1590,7 +1602,7 @@ class calendar_event(osv.Model): else: data = self.read(cr, uid, event_id, ['start', 'stop', 'rrule', 'duration']) if data.get('rrule'): - new_ids = [self._detach_one_event(cr, uid, event_id, values, context=None)] + new_ids = [self._detach_one_event(cr, uid, event_id, values, context=dont_notify_ctx)] super(calendar_event, self).write(cr, uid, real_ids, values, context=context) @@ -1603,7 +1615,18 @@ class calendar_event(osv.Model): attendees_create = False if values.get('partner_ids', False): - attendees_create = self.create_attendees(cr, uid, real_ids + new_ids, context) + attendees_create = self.create_attendees(cr, uid, real_ids + new_ids, context=dont_notify_ctx) + + # Notify attendees if there is an alarm on the modified event, or if there was an alarm + # that has just been removed, as it might have changed their next event notification + if not context.get('dont_notify'): + event = self.browse(cr, uid, event_id, context=context) + if len(event.alarm_ids) > 0 or values.get('alarm_ids'): + partners_to_notify = set([p.id for p in event.partner_ids]) + event_attendees_changes = attendees_create and attendees_create[real_ids[0]] + if event_attendees_changes: + partners_to_notify.update(event_attendees_changes['removed_partner_ids']) + self.pool['calendar.alarm_manager'].notify_next_alarm(cr, uid, partners_to_notify) if (values.get('start_date') or values.get('start_datetime')) and values.get('active', True): for the_id in real_ids + new_ids: @@ -1629,10 +1652,20 @@ class calendar_event(osv.Model): res = super(calendar_event, self).create(cr, uid, vals, context=context) - final_date = self._get_recurrency_end_date(cr, uid, res, context=context) - self.write(cr, uid, [res], {'final_date': final_date}, context=context) + dont_notify_context = dict(context, dont_notify=True) # to prevent multiple notify_next_alarm + + final_date = self._get_recurrency_end_date(cr, uid, res, context=context) + self.write(cr, uid, [res], {'final_date': final_date}, context=dont_notify_context) + + self.create_attendees(cr, uid, [res], context=dont_notify_context) + + # Notify attendees if there is an alarm on the created event, as it might have changed their + # next event notification + if not context.get('dont_notify'): + event = self.browse(cr, uid, [res], context=context) # remove this when migrating to new API + if len(event.alarm_ids) > 0: + self.pool['calendar.alarm_manager'].notify_next_alarm(cr, uid, [p.id for p in event.partner_ids]) - self.create_attendees(cr, uid, [res], context=context) return res def export_data(self, cr, uid, ids, *args, **kwargs): @@ -1723,6 +1756,14 @@ class calendar_event(osv.Model): ids_to_exclure = [] ids_to_unlink = [] + # Get concerned attendees to notify them if there is an alarm on the unlinked events, + # as it might have changed their next event notification + event_ids = self.search(cr, uid, [('id', 'in', ids), ('alarm_ids', '!=', False)], context=context) + events = self.browse(cr, uid, event_ids, context=context) + partner_ids = set() + for event in events: + partner_ids.update([p.id for p in event.partner_ids]) + for event_id in ids: if can_be_deleted and len(str(event_id).split('-')) == 1: # if ID REAL if self.browse(cr, uid, int(event_id), context).recurrent_id: @@ -1736,8 +1777,12 @@ class calendar_event(osv.Model): res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context) if ids_to_exclure: + ctx = dict(context, dont_notify=True) # to prevent multiple notify_next_alarm for id_to_exclure in ids_to_exclure: - res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context) + res = self.write(cr, uid, id_to_exclure, {'active': False}, context=ctx) + + # Notify the concerned attendees (must be done after removing the events) + self.pool['calendar.alarm_manager'].notify_next_alarm(cr, uid, partner_ids) return res diff --git a/addons/calendar/controllers/__init__.py b/addons/calendar/controllers/__init__.py index 8ee9bae18d9..4c1edfdff5d 100644 --- a/addons/calendar/controllers/__init__.py +++ b/addons/calendar/controllers/__init__.py @@ -1 +1,2 @@ import main +import bus diff --git a/addons/calendar/controllers/bus.py b/addons/calendar/controllers/bus.py new file mode 100644 index 00000000000..e74418fce8d --- /dev/null +++ b/addons/calendar/controllers/bus.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -* +import openerp +from openerp.http import request + + +class CalendarBusController(openerp.addons.bus.controllers.main.BusController): + # -------------------------- + # Extends BUS Controller Poll + # -------------------------- + def _poll(self, dbname, channels, last, options): + if request.session.uid: + channels.append((request.db, 'calendar.alarm', request.env.user.partner_id.id)) + return super(CalendarBusController, self)._poll(dbname, channels, last, options) diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index b3f5c5df8af..603790e8768 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -1,6 +1,7 @@ odoo.define('base_calendar.base_calendar', function (require) { "use strict"; +var bus = require('bus.bus').bus; var core = require('web.core'); var CalendarView = require('web_calendar.CalendarView'); var data = require('web.data'); @@ -189,43 +190,60 @@ var CalendarNotification = Notification.extend({ }); WebClient.include({ - get_next_notif: function() { + display_calendar_notif: function(notifications) { var self = this; + var last_notif_timer = 0; - this.rpc("/calendar/notify", {}, {shadow: true}) - .done(function(result) { - _.each(result, function(res) { - setTimeout(function() { - // If notification not already displayed, we create and display it (FIXME is this check usefull?) - if(self.$(".eid_" + res.event_id).length === 0) { - self.notification_manager.display(new CalendarNotification(self.notification_manager, res.title, res.message, res.event_id)); - } - }, res.timer * 1000); - }); - }) - .fail(function(err, ev) { - if(err.code === -32098) { - // Prevent the CrashManager to display an error - // in case of an xhr error not due to a server error - ev.preventDefault(); + // Clear previously set timeouts and destroy currently displayed calendar notifications + clearTimeout(this.get_next_calendar_notif_timeout); + _.each(this.calendar_notif_timeouts, clearTimeout); + _.each(this.calendar_notif, function(notif) { + if (!notif.isDestroyed()) { + notif.destroy(); } }); + this.calendar_notif_timeouts = {}; + this.calendar_notif = {}; + + // For each notification, set a timeout to display it + _.each(notifications, function(notif) { + self.calendar_notif_timeouts[notif.event_id] = setTimeout(function() { + var notification = new CalendarNotification(self.notification_manager, notif.title, notif.message, notif.event_id); + self.notification_manager.display(notification); + self.calendar_notif[notif.event_id] = notification; + }, notif.timer * 1000); + last_notif_timer = Math.max(last_notif_timer, notif.timer); + }); + + // Set a timeout to get the next notifications when the last one has been displayed + if (last_notif_timer > 0) { + this.get_next_calendar_notif_timeout = setTimeout(this.get_next_calendar_notif.bind(this), last_notif_timer * 1000); + } }, - check_notifications: function() { - var self = this; - this.get_next_notif(); - this.intervalNotif = setInterval(function() { - self.get_next_notif(); - }, 5 * 60 * 1000); + get_next_calendar_notif: function() { + this.rpc("/calendar/notify", {}, {shadow: true}) + .done(this.display_calendar_notif.bind(this)) + .fail(function(err, ev) { + if(err.code === -32098) { + // Prevent the CrashManager to display an error + // in case of an xhr error not due to a server error + ev.preventDefault(); + } + }); }, show_application: function() { - return this._super.apply(this, arguments).then(this.check_notifications.bind(this)); - }, - //Override addons/web/static/src/js/chrome.js - // FIXME: on_logout is no longer used - on_logout: function() { - this._super(); - clearInterval(this.intervalNotif); + // An event is triggered on the bus each time a calendar event with alarm + // in which the current user is involved is created, edited or deleted + this.calendar_notif_timeouts = {}; + this.calendar_notif = {}; + bus.on('notification', this, function (notifications) { + _.each(notifications, (function (notification) { + if (notification[0][1] === 'calendar.alarm') { + this.display_calendar_notif(notification[1]); + } + }).bind(this)); + }); + return this._super.apply(this, arguments).then(this.get_next_calendar_notif.bind(this)); }, });