summaryrefslogtreecommitdiff
path: root/addons/calendar/models
diff options
context:
space:
mode:
authorLucas Lefèvre <lul@odoo.com>2019-12-10 09:38:43 +0100
committerLucas Lefèvre <lul@odoo.com>2020-04-09 10:56:37 +0200
commit7159f657a009e461ffc16408db40066b901885d1 (patch)
tree940953b37a7ac190de3f3da055d865d382cab100 /addons/calendar/models
parenta27afdb5434166c3ea48c18ccfba9e8245d18e62 (diff)
[REF] calendar: Refactor calendar attendees management
This commit refactors the event attendees and notifications according to the recurring event refactoring. Some (very) basic reminder tests are also added. Task 2126717 PR #42031 PR Enterprise odoo/enterprise#8006
Diffstat (limited to 'addons/calendar/models')
-rw-r--r--addons/calendar/models/calendar_alarm_manager.py24
-rw-r--r--addons/calendar/models/calendar_attendee.py101
-rw-r--r--addons/calendar/models/calendar_event.py143
-rw-r--r--addons/calendar/models/calendar_recurrence.py30
-rw-r--r--addons/calendar/models/ir_http.py2
5 files changed, 158 insertions, 142 deletions
diff --git a/addons/calendar/models/calendar_alarm_manager.py b/addons/calendar/models/calendar_alarm_manager.py
index 943073c11c2..98fe461b7cd 100644
--- a/addons/calendar/models/calendar_alarm_manager.py
+++ b/addons/calendar/models/calendar_alarm_manager.py
@@ -15,7 +15,7 @@ class AlarmManager(models.AbstractModel):
_name = 'calendar.alarm_manager'
_description = 'Event Alarm Manager'
- def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partner_id=None):
+ def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partners=None):
result = {}
delta_request = """
SELECT
@@ -50,16 +50,16 @@ class AlarmManager(models.AbstractModel):
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 = %s
+ AND part_rel.res_partner_id IN %s
"""
# Add filter on alarm type
tuple_params = (alarm_type,)
# Add filter on partner_id
- if partner_id:
+ if partners:
base_request += filter_user
- tuple_params += (partner_id, )
+ tuple_params += (tuple(partners.ids), )
# Upper bound on first_alarm of requested events
first_alarm_max_value = ""
@@ -117,10 +117,10 @@ class AlarmManager(models.AbstractModel):
"""
result = []
# TODO: remove event_maxdelta and if using it
- if one_date - timedelta(minutes=(missing and 0 or event_maxdelta)) < datetime.datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
+ if one_date - timedelta(minutes=(missing and 0 or event_maxdelta)) < fields.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.alarm_type == alarm_type and \
- one_date - timedelta(minutes=(missing and 0 or alarm.duration_minutes)) < datetime.datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
+ one_date - timedelta(minutes=(missing and 0 or alarm.duration_minutes)) < fields.Datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
(not after or one_date - timedelta(minutes=alarm.duration_minutes) > fields.Datetime.from_string(after)):
alert = {
'alarm_id': alarm.id,
@@ -132,6 +132,10 @@ class AlarmManager(models.AbstractModel):
@api.model
def get_next_mail(self):
+ return self._get_partner_next_mail(partners=None)
+
+ @api.model
+ def _get_partner_next_mail(self, partners=None):
last_notif_mail = fields.Datetime.to_string(self.env.context.get('lastcall') or fields.Datetime.now())
cron = self.env.ref('calendar.ir_cron_scheduler_alarm', raise_if_not_found=False)
@@ -153,7 +157,7 @@ class AlarmManager(models.AbstractModel):
cron_interval = cron.interval_number * interval_to_second[cron.interval_type]
- all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval)
+ all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval, partners=partners)
for meeting in self.env['calendar.event'].browse(all_meetings):
max_delta = all_meetings[meeting.id]['max_duration']
@@ -183,7 +187,7 @@ class AlarmManager(models.AbstractModel):
if not partner:
return []
- all_meetings = self._get_next_potential_limit_alarm('notification', partner_id=partner.id)
+ 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']
@@ -215,7 +219,7 @@ class AlarmManager(models.AbstractModel):
result = False
if alarm.alarm_type == 'email':
- result = meeting.attendee_ids.filtered(lambda r: r.state != 'declined')._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True, force_event_id=meeting)
+ result = meeting.attendee_ids.filtered(lambda r: r.state != 'declined')._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True, ignore_recurrence=True)
return result
def do_notif_reminder(self, alert):
@@ -225,7 +229,7 @@ class AlarmManager(models.AbstractModel):
if alarm.alarm_type == 'notification':
message = meeting.display_time
- delta = alert['notify_at'] - datetime.datetime.now()
+ delta = alert['notify_at'] - fields.Datetime.now()
delta = delta.seconds + delta.days * 3600 * 24
return {
diff --git a/addons/calendar/models/calendar_attendee.py b/addons/calendar/models/calendar_attendee.py
index 765258274f0..375a18a1057 100644
--- a/addons/calendar/models/calendar_attendee.py
+++ b/addons/calendar/models/calendar_attendee.py
@@ -2,10 +2,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
import base64
+import logging
+from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
+_logger = logging.getLogger(__name__)
+
class Attendee(models.Model):
""" Calendar Attendee Information """
@@ -24,43 +28,59 @@ class Attendee(models.Model):
('accepted', 'Accepted'),
]
+ event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, ondelete='cascade')
+ partner_id = fields.Many2one('res.partner', 'Contact', required=True, readonly=True)
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction',
help="Status of the attendee's participation")
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
- partner_id = fields.Many2one('res.partner', 'Contact', readonly=True)
- email = fields.Char('Email', help="Email of Invited Person")
+ email = fields.Char('Email', related='partner_id.email', help="Email of Invited Person")
availability = fields.Selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly=True)
access_token = fields.Char('Invitation Token', default=_default_access_token)
- event_id = fields.Many2one('calendar.event', 'Meeting linked', ondelete='cascade')
+ recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
@api.depends('partner_id', 'partner_id.name', 'email')
def _compute_common_name(self):
for attendee in self:
attendee.common_name = attendee.partner_id.name or attendee.email
- @api.onchange('partner_id')
- def _onchange_partner_id(self):
- """ Make entry on email and availability on change of partner_id field. """
- self.email = self.partner_id.email
-
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
+ if values.get('partner_id') == self.env.user.partner_id.id:
+ values['state'] = 'accepted'
if not values.get("email") and values.get("common_name"):
common_nameval = values.get("common_name").split(':')
email = [x for x in common_nameval if '@' in x] # TODO JEM : should be refactored
values['email'] = email and email[0] or ''
values['common_name'] = values.get("common_name")
- return super(Attendee, self).create(vals_list)
+ attendees = super().create(vals_list)
+ attendees._subscribe_partner()
+ return attendees
+
+ def unlink(self):
+ self._unsubscribe_partner()
+ return super().unlink()
+
+ def _subscribe_partner(self):
+ for event in self.event_id:
+ partners = (event.attendee_ids & self).partner_id - event.message_partner_ids
+ partners -= self.env.user.partner_id # current user is automatically added as followers, don't add it twice.
+ event.message_subscribe(partner_ids=partners.ids)
+
+ def _unsubscribe_partner(self):
+ for event in self.event_id:
+ partners = (event.attendee_ids & self).partner_id & event.message_partner_ids
+ event.message_unsubscribe(partner_ids=partners.ids)
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
raise UserError(_('You cannot duplicate a calendar attendee.'))
- def _send_mail_to_attendees(self, template_xmlid, force_send=False, force_event_id=None):
+ def _send_mail_to_attendees(self, template_xmlid, force_send=False, ignore_recurrence=False):
""" Send mail for event invitation to event attendees.
:param template_xmlid: xml id of the email template to use to send the invitation
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
+ :param ignore_recurrence: ignore event recurrence
"""
res = False
@@ -68,10 +88,12 @@ class Attendee(models.Model):
return res
calendar_view = self.env.ref('calendar.view_calendar_event_calendar')
- invitation_template = self.env.ref(template_xmlid)
-
+ invitation_template = self.env.ref(template_xmlid, raise_if_not_found=False)
+ if not invitation_template:
+ _logger.warning("Template %s could not be found. %s not notified." % (template_xmlid, self))
+ return
# get ics file for all meetings
- ics_files = force_event_id._get_ics_file() if force_event_id else self.mapped('event_id')._get_ics_file()
+ ics_files = self.mapped('event_id')._get_ics_file()
# prepare rendering context for mail template
colors = {
@@ -82,40 +104,41 @@ class Attendee(models.Model):
}
rendering_context = dict(self._context)
rendering_context.update({
- 'color': colors,
+ 'colors': colors,
+ 'ignore_recurrence': ignore_recurrence,
'action_id': self.env['ir.actions.act_window'].search([('view_id', '=', calendar_view.id)], limit=1).id,
'dbname': self._cr.dbname,
'base_url': self.env['ir.config_parameter'].sudo().get_param('web.base.url', default='http://localhost:8069'),
- 'force_event_id': force_event_id,
})
- invitation_template = invitation_template.with_context(rendering_context)
- # send email with attachments
- mail_ids = []
for attendee in self:
- if attendee.email or attendee.partner_id.email:
+ if attendee.email and attendee.partner_id != self.env.user.partner_id:
# FIXME: is ics_file text or bytes?
- event_id = force_event_id.id if force_event_id else attendee.event_id.id
+ event_id = attendee.event_id.id
ics_file = ics_files.get(event_id)
- email_values = {
- 'model': None, # We don't want to have the mail in the tchatter while in queue!
- 'res_id': None,
- }
if ics_file:
- email_values['attachment_ids'] = [
+ attachment_values = [
(0, 0, {'name': 'invitation.ics',
'mimetype': 'text/calendar',
'datas': base64.b64encode(ics_file)})
]
- mail_ids.append(invitation_template.with_context(no_document=True).send_mail(attendee.id, email_values=email_values, notif_layout='mail.mail_notification_light'))
- else:
- mail_ids.append(invitation_template.send_mail(attendee.id, email_values=email_values, notif_layout='mail.mail_notification_light'))
-
- if force_send and mail_ids:
- res = self.env['mail.mail'].browse(mail_ids).send()
-
- return res
+ body = self.env['mail.template'].with_context(rendering_context)._render_template(
+ invitation_template.body_html,
+ self._name,
+ attendee.ids)
+ # TODO check lang
+ subject = self.env['mail.template']._render_template(
+ invitation_template.subject,
+ self._name,
+ attendee.ids)
+ attendee.event_id.with_context(no_document=True).message_notify(
+ body=body,
+ subject=subject,
+ partner_ids=attendee.partner_id.ids,
+ email_layout_xmlid='mail.mail_notification_light',
+ attachment_ids=attachment_values,
+ force_send=force_send)
def do_tentative(self):
""" Makes event invitation as Tentative. """
@@ -123,17 +146,13 @@ class Attendee(models.Model):
def do_accept(self):
""" Marks event invitation as Accepted. """
- result = self.write({'state': 'accepted'})
for attendee in self:
- if attendee.event_id:
- attendee.event_id.message_post(body=_("%s has accepted invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
- return result
+ attendee.event_id.message_post(body=_("%s has accepted invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
+ return self.write({'state': 'accepted'})
def do_decline(self):
""" Marks event invitation as Declined. """
- res = self.write({'state': 'declined'})
for attendee in self:
- if attendee.event_id:
- attendee.event_id.message_post(body=_("%s has declined invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
- return res
+ attendee.event_id.message_post(body=_("%s has declined invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
+ return self.write({'state': 'declined'})
diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py
index 84842effcae..98d92296257 100644
--- a/addons/calendar/models/calendar_event.py
+++ b/addons/calendar/models/calendar_event.py
@@ -448,61 +448,37 @@ class Meeting(models.Model):
return result
- def create_attendees(self):
- current_user = self.env.user
- result = {}
- for meeting in self:
- alreay_meeting_partners = meeting.attendee_ids.mapped('partner_id')
- meeting_attendees = self.env['calendar.attendee']
- meeting_partners = self.env['res.partner']
- for partner in meeting.partner_ids.filtered(lambda partner: partner not in alreay_meeting_partners):
- values = {
- 'partner_id': partner.id,
- 'email': partner.email,
- 'event_id': meeting.id,
- }
-
- # TODO move this in the correct module
- if self._context.get('google_internal_event_id', False):
- values['google_internal_event_id'] = self._context.get('google_internal_event_id')
-
- # current user don't have to accept his own meeting
- if partner == self.env.user.partner_id:
- values['state'] = 'accepted'
-
- attendee = self.env['calendar.attendee'].create(values)
-
- meeting_attendees |= attendee
- meeting_partners |= partner
-
- if meeting_attendees and not self._context.get('detaching'):
- to_notify = meeting_attendees.filtered(lambda a: a.email != current_user.email)
- to_notify._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
-
- if meeting_attendees:
- meeting.write({'attendee_ids': [(4, meeting_attendee.id) for meeting_attendee in meeting_attendees]})
-
- if meeting_partners:
- meeting.message_subscribe(partner_ids=meeting_partners.ids)
-
- # We remove old attendees who are not in partner_ids now.
- all_partners = meeting.partner_ids
- all_partner_attendees = meeting.attendee_ids.mapped('partner_id')
- old_attendees = meeting.attendee_ids
- partners_to_remove = all_partner_attendees + meeting_partners - all_partners
-
- attendees_to_remove = self.env["calendar.attendee"]
- if partners_to_remove:
- attendees_to_remove = self.env["calendar.attendee"].search([('partner_id', 'in', partners_to_remove.ids), ('event_id', '=', meeting.id)])
- attendees_to_remove.unlink()
-
- result[meeting.id] = {
- 'new_attendees': meeting_attendees,
- 'old_attendees': old_attendees,
- 'removed_attendees': attendees_to_remove,
- 'removed_partners': partners_to_remove
- }
- return result
+ def _attendees_values(self, partner_commands):
+ """
+ :param partner_commands: ORM commands for partner_id field (0 and 1 commands not supported)
+ :return: associated attendee_ids ORM commands
+ """
+ attendee_commands = []
+
+ removed_partner_ids = []
+ added_partner_ids = []
+ for command in partner_commands:
+ op = command[0]
+ if op in (2, 3): # Remove partner
+ removed_partner_ids += [command[1]]
+ elif op == 6: # Replace all
+ removed_partner_ids += set(self.partner_ids.ids) - set(command[2]) # Don't recreate attendee if partner already attend the event
+ added_partner_ids += set(command[2]) - set(self.partner_ids.ids)
+ elif op == 4:
+ added_partner_ids += [command[1]] if command[1] not in self.partner_ids.ids else []
+ # commands 0 and 1 not supported
+
+ attendees_to_unlink = self.env['calendar.attendee'].search([
+ ('event_id', 'in', self.ids),
+ ('partner_id', 'in', removed_partner_ids),
+ ])
+ attendee_commands += [[2, attendee.id] for attendee in attendees_to_unlink] # Removes and delete
+
+ attendee_commands += [
+ [0, 0, dict(partner_id=partner_id)]
+ for partner_id in added_partner_ids
+ ]
+ return attendee_commands
def get_interval(self, interval, tz=None):
""" Format and localize some dates to be used in email templates
@@ -646,6 +622,11 @@ class Meeting(models.Model):
break_recurrence = values.get('recurrency') is False
self._sync_activities(values)
+ if 'partner_ids' in values:
+ values['attendee_ids'] = self._attendees_values(values['partner_ids'])
+
+ previous_attendees = self.attendee_ids
+
recurrence_values = {field: values.pop(field) for field in self._get_recurrent_fields() if field in values}
if update_recurrence:
if break_recurrence:
@@ -660,39 +641,24 @@ class Meeting(models.Model):
else:
super().write(values)
+ if recurrence_update_setting != 'self_only' and not break_recurrence:
+ detached_events |= self._apply_recurrence_values(recurrence_values, future=recurrence_update_setting == 'future_events')
- for meeting in self:
-
- attendees_create = False
- if values.get('partner_ids'):
- attendees_create = self.with_context(dont_notify=True).create_attendees() # to prevent multiple _notify_next_alarm
-
- # 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 self._context.get('dont_notify'):
- if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'):
- partners_to_notify = meeting.partner_ids.ids
- event_attendees_changes = attendees_create and meeting and attendees_create[meeting.id]
- if event_attendees_changes:
- partners_to_notify.extend(event_attendees_changes['removed_partners'].ids)
- self.env['calendar.alarm_manager']._notify_next_alarm(partners_to_notify)
-
- if (values.get('start_date') or values.get('start_datetime') or
- (values.get('start') and self.env.context.get('from_ui'))) and values.get('active', True):
- for current_meeting in self:
- if attendees_create:
- attendees_create = attendees_create[current_meeting.id]
- attendee_to_email = attendees_create['old_attendees'] - attendees_create['removed_attendees']
- else:
- attendee_to_email = current_meeting.attendee_ids
-
- if attendee_to_email:
- attendee_to_email._send_mail_to_attendees('calendar.calendar_template_meeting_changedate')
-
- if recurrence_update != 'self_only' and not break_recurrence:
- detached_events |= self._apply_recurrence_values(recurrence_values, future=recurrence_update == 'future_events')
(detached_events & self).active = False
(detached_events - self).unlink()
+
+ # 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 self._context.get('dont_notify'):
+ if self.alarm_ids or values.get('alarm_ids'):
+ self.env['calendar.alarm_manager']._notify_next_alarm(self.partner_ids.ids)
+
+ current_attendees = self.filtered('active').attendee_ids
+ if 'partner_ids' in values:
+ (current_attendees - previous_attendees)._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
+ if 'start' in values:
+ (current_attendees & previous_attendees)._send_mail_to_attendees('calendar.calendar_template_meeting_changedate', ignore_recurrence=not update_recurrence)
+
return True
@api.model # LUL TODO create multi
@@ -719,6 +685,9 @@ class Meeting(models.Model):
activity_vals['user_id'] = user_id
values['activity_ids'] = [(0, 0, activity_vals)]
+ if 'partner_ids' in values:
+ values['attendee_ids'] = self._attendees_values(values['partner_ids'])
+
recurrence_values = {field: values.pop(field) for field in self._get_recurrent_fields() if field in values}
meeting = super(Meeting, self).create(values)
@@ -726,11 +695,9 @@ class Meeting(models.Model):
detached_events = meeting._apply_recurrence_values(recurrence_values)
detached_events.active = False
+ meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
meeting._sync_activities(values)
- # `dont_notify=True` in context to prevent multiple _notify_next_alarm
- meeting.with_context(dont_notify=True).create_attendees()
-
# Notify attendees if there is an alarm on the created event, as it might have changed their
# next event notification
if not self._context.get('dont_notify'):
diff --git a/addons/calendar/models/calendar_recurrence.py b/addons/calendar/models/calendar_recurrence.py
index 2553b43218d..acc5241ac42 100644
--- a/addons/calendar/models/calendar_recurrence.py
+++ b/addons/calendar/models/calendar_recurrence.py
@@ -90,8 +90,7 @@ class RecurrenceRule(models.Model):
_name = 'calendar.recurrence'
_description = 'Event Recurrence Rule'
- # TODO exdate?
-
+ name = fields.Char(compute='_compute_name', store=True)
base_event_id = fields.Many2one('calendar.event', ondelete='set null', copy=False)
calendar_event_ids = fields.One2many('calendar.event', 'recurrence_id')
event_tz = fields.Selection(_tz_get, string='Timezone', default=lambda self: self.env.context.get('tz') or self.env.user.tz)
@@ -118,6 +117,33 @@ class RecurrenceRule(models.Model):
('month_day', "CHECK (rrule_type != 'monthly' OR month_by != 'day' OR day >= 1 AND day <= 31)", "The day must be between 1 and 31"),
]
+ @api.depends('rrule')
+ def _compute_name(self):
+ for recurrence in self:
+ period = dict(RRULE_TYPE_SELECTION)[recurrence.rrule_type]
+ every = _("Every %s %s, ") % (recurrence.interval, period)
+
+ if recurrence.end_type == 'count':
+ end = _("for %s events") % recurrence.count
+ elif recurrence.end_type == 'end_date':
+ end = _("until %s") % recurrence.until
+ else:
+ end = ''
+
+ if recurrence.rrule_type == 'weeky':
+ weekdays = recurrence._get_week_days()
+ fields = (self._fields[weekday_to_field(w)] for w in weekdays)
+ on = _("on %s,") % ", ".join([field.string for field in fields])
+ elif recurrence.rrule_type == 'monthly':
+ if recurrence.month_by == 'day':
+ weekday_label = dict(BYDAY_SELECTION)[recurrence.byday]
+ on = _("on the %(position)s %(weekday)s, ") % {'position': recurrence.byday, 'weekday': weekday_label}
+ else:
+ on = _("day %s, ") % recurrence.day
+ else:
+ on = ''
+ recurrence.name = every + on + end
+
@api.depends('calendar_event_ids.start')
def _compute_dtstart(self):
groups = self.env['calendar.event'].read_group([('recurrence_id', 'in', self.ids)], ['start:min'], ['recurrence_id'])
diff --git a/addons/calendar/models/ir_http.py b/addons/calendar/models/ir_http.py
index f28088382a1..07441f15135 100644
--- a/addons/calendar/models/ir_http.py
+++ b/addons/calendar/models/ir_http.py
@@ -32,4 +32,4 @@ class IrHttp(models.AbstractModel):
if error_message:
raise BadRequest(error_message)
- return True
+ cls._auth_method_public()