diff options
| author | Lucas Lefèvre <lul@odoo.com> | 2019-12-10 09:38:43 +0100 |
|---|---|---|
| committer | Lucas Lefèvre <lul@odoo.com> | 2020-04-09 10:56:37 +0200 |
| commit | 7159f657a009e461ffc16408db40066b901885d1 (patch) | |
| tree | 940953b37a7ac190de3f3da055d865d382cab100 /addons/calendar/models | |
| parent | a27afdb5434166c3ea48c18ccfba9e8245d18e62 (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.py | 24 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_attendee.py | 101 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_event.py | 143 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_recurrence.py | 30 | ||||
| -rw-r--r-- | addons/calendar/models/ir_http.py | 2 |
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() |
