251 lines
11 KiB
Python
251 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Talisman . See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
from datetime import timedelta, datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
from pytz import UTC
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.tools import plaintext2html
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AlarmManager(models.AbstractModel):
|
|
_name = 'calendar.alarm_manager'
|
|
_description = 'Event Alarm Manager'
|
|
|
|
def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partners=None):
|
|
# flush models before making queries
|
|
for model_name in ('calendar.alarm', 'calendar.event', 'calendar.recurrence'):
|
|
self.env[model_name].flush_model()
|
|
|
|
result = {}
|
|
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.alarm_type = %s
|
|
GROUP BY rel.calendar_event_id
|
|
"""
|
|
base_request = """
|
|
SELECT
|
|
cal.id,
|
|
cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
|
|
CASE
|
|
WHEN cal.recurrency THEN rrule.until - interval '1' minute * calcul_delta.min_delta
|
|
ELSE cal.stop - interval '1' minute * calcul_delta.min_delta
|
|
END as last_alarm,
|
|
cal.start as first_event_date,
|
|
CASE
|
|
WHEN cal.recurrency THEN rrule.until
|
|
ELSE cal.stop
|
|
END as last_event_date,
|
|
calcul_delta.min_delta,
|
|
calcul_delta.max_delta,
|
|
rrule.rrule AS rule
|
|
FROM
|
|
calendar_event AS cal
|
|
RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id
|
|
LEFT JOIN calendar_recurrence as rrule ON rrule.id = cal.recurrence_id
|
|
"""
|
|
|
|
filter_user = """
|
|
RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
|
|
AND part_rel.res_partner_id IN %s
|
|
"""
|
|
|
|
# Add filter on alarm type
|
|
tuple_params = (alarm_type,)
|
|
|
|
# Add filter on partner_id
|
|
if partners:
|
|
base_request += filter_user
|
|
tuple_params += (tuple(partners.ids), )
|
|
|
|
# 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,)
|
|
|
|
self.env.flush_all()
|
|
self._cr.execute("""
|
|
WITH calcul_delta AS (%s)
|
|
SELECT *
|
|
FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS
|
|
WHERE ALL_EVENTS.first_alarm < %s
|
|
AND ALL_EVENTS.last_event_date > (now() at time zone 'utc')
|
|
""" % (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 self._cr.fetchall():
|
|
result[event_id] = {
|
|
'event_id': event_id,
|
|
'first_alarm': first_alarm,
|
|
'last_alarm': last_alarm,
|
|
'first_meeting': first_meeting,
|
|
'last_meeting': last_meeting,
|
|
'min_duration': min_duration,
|
|
'max_duration': max_duration,
|
|
'rrule': rule
|
|
}
|
|
|
|
# determine accessible events
|
|
events = self.env['calendar.event'].browse(result)
|
|
result = {
|
|
key: result[key]
|
|
for key in set(events._filter_access_rules('read').ids)
|
|
}
|
|
return result
|
|
|
|
def do_check_alarm_for_one_date(self, one_date, event, event_maxdelta, in_the_next_X_seconds, alarm_type, after=False, missing=False):
|
|
""" Search for some alarms in the interval of time determined by some parameters (after, in_the_next_X_seconds, ...)
|
|
:param one_date: date of the event to check (not the same that in the event browse if recurrent)
|
|
:param event: Event browse record
|
|
:param event_maxdelta: biggest duration from alarms for this event
|
|
:param in_the_next_X_seconds: looking in the future (in seconds)
|
|
:param after: if not False: will return alert if after this date (date as string - todo: change in master)
|
|
:param missing: if not False: will return alert even if we are too late
|
|
:param notif: Looking for type notification
|
|
:param mail: looking for type email
|
|
"""
|
|
result = []
|
|
# TODO: remove event_maxdelta and if using it
|
|
past = one_date - timedelta(minutes=(missing * event_maxdelta))
|
|
future = fields.Datetime.now() + timedelta(seconds=in_the_next_X_seconds)
|
|
if future <= past:
|
|
return result
|
|
for alarm in event.alarm_ids:
|
|
if alarm.alarm_type != alarm_type:
|
|
continue
|
|
past = one_date - timedelta(minutes=(missing * alarm.duration_minutes))
|
|
if future <= past:
|
|
continue
|
|
if after and past <= fields.Datetime.from_string(after):
|
|
continue
|
|
result.append({
|
|
'alarm_id': alarm.id,
|
|
'event_id': event.id,
|
|
'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
|
|
})
|
|
return result
|
|
|
|
def _get_events_by_alarm_to_notify(self, alarm_type):
|
|
"""
|
|
Get the events with an alarm of the given type between the cron
|
|
last call and now.
|
|
|
|
Please note that all new reminders created since the cron last
|
|
call with an alarm prior to the cron last call are skipped by
|
|
design. The attendees receive an invitation for any new event
|
|
already.
|
|
"""
|
|
lastcall = self.env.context.get('lastcall', False) or fields.date.today() - relativedelta(weeks=1)
|
|
now = datetime.now(tz=UTC)
|
|
self.env.cr.execute('''
|
|
SELECT "alarm"."id", "event"."id"
|
|
FROM "calendar_event" AS "event"
|
|
JOIN "calendar_alarm_calendar_event_rel" AS "event_alarm_rel"
|
|
ON "event"."id" = "event_alarm_rel"."calendar_event_id"
|
|
JOIN "calendar_alarm" AS "alarm"
|
|
ON "event_alarm_rel"."calendar_alarm_id" = "alarm"."id"
|
|
WHERE (
|
|
"alarm"."alarm_type" = %s
|
|
AND "event"."active"
|
|
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) >= %s
|
|
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) < %s
|
|
)''', [alarm_type, lastcall, now])
|
|
|
|
events_by_alarm = {}
|
|
for alarm_id, event_id in self.env.cr.fetchall():
|
|
events_by_alarm.setdefault(alarm_id, list()).append(event_id)
|
|
return events_by_alarm
|
|
|
|
@api.model
|
|
def _send_reminder(self):
|
|
# Executed via cron
|
|
events_by_alarm = self._get_events_by_alarm_to_notify('email')
|
|
if not events_by_alarm:
|
|
return
|
|
|
|
event_ids = list(set(event_id for event_ids in events_by_alarm.values() for event_id in event_ids))
|
|
events = self.env['calendar.event'].browse(event_ids)
|
|
attendees = events.attendee_ids.filtered(lambda a: a.state != 'declined')
|
|
alarms = self.env['calendar.alarm'].browse(events_by_alarm.keys())
|
|
for alarm in alarms:
|
|
alarm_attendees = attendees.filtered(lambda attendee: attendee.event_id.id in events_by_alarm[alarm.id])
|
|
alarm_attendees.with_context(
|
|
calendar_template_ignore_recurrence=True
|
|
)._send_mail_to_attendees(
|
|
alarm.mail_template_id,
|
|
force_send=True
|
|
)
|
|
|
|
for event in events:
|
|
if event.recurrence_id:
|
|
next_date = event.get_next_alarm_date(events_by_alarm)
|
|
event.recurrence_id.with_context(date=next_date)._setup_alarms()
|
|
|
|
@api.model
|
|
def get_next_notif(self):
|
|
partner = self.env.user.partner_id
|
|
all_notif = []
|
|
|
|
if not partner:
|
|
return []
|
|
|
|
all_meetings = self._get_next_potential_limit_alarm('notification', partners=partner)
|
|
time_limit = 3600 * 24 # return alarms of the next 24 hours
|
|
for event_id in all_meetings:
|
|
max_delta = all_meetings[event_id]['max_duration']
|
|
meeting = self.env['calendar.event'].browse(event_id)
|
|
in_date_format = fields.Datetime.from_string(meeting.start)
|
|
last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, time_limit, 'notification', after=partner.calendar_last_notif_ack)
|
|
if last_found:
|
|
for alert in last_found:
|
|
all_notif.append(self.do_notif_reminder(alert))
|
|
return all_notif
|
|
|
|
def do_notif_reminder(self, alert):
|
|
alarm = self.env['calendar.alarm'].browse(alert['alarm_id'])
|
|
meeting = self.env['calendar.event'].browse(alert['event_id'])
|
|
|
|
if alarm.alarm_type == 'notification':
|
|
message = meeting.display_time
|
|
if alarm.body:
|
|
message += '<p>%s</p>' % plaintext2html(alarm.body)
|
|
|
|
delta = alert['notify_at'] - fields.Datetime.now()
|
|
delta = delta.seconds + delta.days * 3600 * 24
|
|
|
|
return {
|
|
'alarm_id': alarm.id,
|
|
'event_id': meeting.id,
|
|
'title': meeting.name,
|
|
'message': message,
|
|
'timer': delta,
|
|
'notify_at': fields.Datetime.to_string(alert['notify_at']),
|
|
}
|
|
|
|
def _notify_next_alarm(self, partner_ids):
|
|
""" Sends through the bus the next alarm of given partners """
|
|
notifications = []
|
|
users = self.env['res.users'].search([('partner_id', 'in', tuple(partner_ids))])
|
|
for user in users:
|
|
notif = self.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).get_next_notif()
|
|
notifications.append([user.partner_id, 'calendar.alarm', notif])
|
|
if len(notifications) > 0:
|
|
self.env['bus.bus']._sendmany(notifications)
|