[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
This commit is contained in:
@@ -24,6 +24,32 @@ class CalendarController(http.Controller):
|
||||
attendee.do_accept()
|
||||
return self.view(db, token, action, id, view='form')
|
||||
|
||||
@http.route('/calendar/recurrence/accept', type='http', auth="calendar")
|
||||
def accept_recurrence(self, db, token, action, id, **kwargs):
|
||||
# LUL TODO db required?
|
||||
attendee = request.env['calendar.attendee'].sudo().search([('access_token', '=', token), ('state', '!=', 'accepted')])
|
||||
if attendee:
|
||||
attendees = request.env['calendar.attendee'].sudo().search([
|
||||
('event_id', 'in', attendee.event_id.recurrence_id.calendar_event_ids.ids),
|
||||
('partner_id', '=', attendee.partner_id.id),
|
||||
('state', '!=', 'accepted'),
|
||||
])
|
||||
attendees.do_accept()
|
||||
return self.view(db, token, action, id, view='form')
|
||||
|
||||
@http.route('/calendar/recurrence/decline', type='http', auth="calendar")
|
||||
def decline_recurrence(self, db, token, action, id, **kwargs):
|
||||
# LUL TODO db required?
|
||||
attendee = request.env['calendar.attendee'].sudo().search([('access_token', '=', token), ('state', '!=', 'declined')])
|
||||
if attendee:
|
||||
attendees = request.env['calendar.attendee'].sudo().search([
|
||||
('event_id', 'in', attendee.event_id.recurrence_id.calendar_event_ids.ids),
|
||||
('partner_id', '=', attendee.partner_id.id),
|
||||
('state', '!=', 'declined'),
|
||||
])
|
||||
attendees.do_decline()
|
||||
return self.view(db, token, action, id, view='form')
|
||||
|
||||
@http.route('/calendar/meeting/decline', type='http', auth="calendar")
|
||||
def declined(self, db, token, action, id):
|
||||
registry = registry_get(db)
|
||||
|
||||
@@ -11,23 +11,26 @@
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
|
||||
% set colors = ctx.get('colors', {})
|
||||
% set recurrent = object.recurrence_id and not ctx['ignore_recurrence']
|
||||
<p>
|
||||
Hello ${object.common_name},<br/><br/>
|
||||
${object.event_id.user_id.partner_id.name} invited you for the ${object.event_id.name} meeting of ${object.event_id.user_id.company_id.name}.
|
||||
</p>
|
||||
<div style="text-align: center; margin: 16px 0px 16px 0px;">
|
||||
<a href="/calendar/meeting/accept?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
% set target = 'recurrence' if recurrent else 'meeting'
|
||||
<a href="/calendar/${target}/accept?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Accept</a>
|
||||
<a href="/calendar/meeting/decline?db=${'dbname' in ctx and ctx['dbname'] or '' }&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
<a href="/calendar/${target}/decline?db=${'dbname' in ctx and ctx['dbname'] or '' }&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Decline</a>
|
||||
<a href="/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
<a href="/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
View</a>
|
||||
</div>
|
||||
<table border="0" cellpadding="0" cellspacing="0"><tr>
|
||||
% if not recurrent:
|
||||
<td width="130px;">
|
||||
<div style="border-top-left-radius: 3px; border-top-right-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
|
||||
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
|
||||
@@ -43,6 +46,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td width="20px;"/>
|
||||
% endif
|
||||
<td style="padding-top: 5px;">
|
||||
<p><strong>Details of the event</strong></p>
|
||||
<ul>
|
||||
@@ -54,6 +58,9 @@
|
||||
% if object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% endif
|
||||
% if recurrent:
|
||||
<li>When: ${object.recurrence_id.name}</li>
|
||||
% endif
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
@@ -95,16 +102,18 @@
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
|
||||
% set colors = ctx.get('colors', {})
|
||||
% set recurrent = object.recurrence_id and not ctx['ignore_recurrence']
|
||||
<p>
|
||||
Hello ${object.common_name},<br/><br/>
|
||||
The date of the meeting has been updated. The meeting ${object.event_id.name} created by ${object.event_id.user_id.partner_id.name} is now scheduled for ${object.event_id.get_display_time_tz(tz=object.partner_id.tz)}.
|
||||
</p>
|
||||
<div style="text-align: center; margin: 16px 0px 16px 0px;">
|
||||
<a href="/calendar/meeting/accept?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
% set target = 'recurrence' if recurrent else 'meeting'
|
||||
<a href="/calendar/${target}/accept?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Accept</a>
|
||||
<a href="/calendar/meeting/decline?db=${'dbname' in ctx and ctx['dbname'] or '' }&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
<a href="/calendar/${target}/decline?db=${'dbname' in ctx and ctx['dbname'] or '' }&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Decline</a>
|
||||
<a href="/calendar/meeting/view?db=${'dbname' in ctx and ctx['dbname'] or ''}&token=${object.access_token}&action=${'action_id' in ctx and ctx['action_id'] or ''}&id=${object.event_id.id}"
|
||||
@@ -112,6 +121,7 @@
|
||||
View</a>
|
||||
</div>
|
||||
<table border="0" cellpadding="0" cellspacing="0"><tr>
|
||||
% if not recurrent:
|
||||
<td width="130px;">
|
||||
<div style="border-top-left-radius: 3px; border-top-right-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
|
||||
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
|
||||
@@ -127,6 +137,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td width="20px;"/>
|
||||
% endif
|
||||
<td style="padding-top: 5px;">
|
||||
<p><strong>Details of the event</strong></p>
|
||||
<ul>
|
||||
@@ -138,6 +149,9 @@
|
||||
% if object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% endif
|
||||
% if recurrent:
|
||||
<li>When: ${object.recurrence_id.name}</li>
|
||||
% endif
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'})
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
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 = []
|
||||
|
||||
# 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')
|
||||
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
|
||||
|
||||
# current user don't have to accept his own meeting
|
||||
if partner == self.env.user.partner_id:
|
||||
values['state'] = 'accepted'
|
||||
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 = 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
|
||||
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'):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -32,4 +32,4 @@ class IrHttp(models.AbstractModel):
|
||||
if error_message:
|
||||
raise BadRequest(error_message)
|
||||
|
||||
return True
|
||||
cls._auth_method_public()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_attendees
|
||||
from . import test_calendar
|
||||
from . import test_calendar_recurrent_event_case2
|
||||
from . import test_event_recurrence
|
||||
from . import test_event_notifications
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo.tests.common import SavepointCase, new_test_user
|
||||
|
||||
|
||||
class TestEventNotifications(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.event = cls.env['calendar.event'].create({
|
||||
'name': "Doom's day",
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
}).with_context(mail_notrack=True)
|
||||
cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
|
||||
cls.partner = cls.user.partner_id
|
||||
|
||||
def test_attendee_added(self):
|
||||
self.event.partner_ids = self.partner
|
||||
self.assertTrue(self.event.attendee_ids, "It should have created an attendee")
|
||||
self.assertEqual(self.event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
|
||||
self.assertIn(self.partner, self.event.message_follower_ids.partner_id, "He should be follower of the event")
|
||||
|
||||
def test_attendee_added_create(self):
|
||||
event = self.env['calendar.event'].create({
|
||||
'name': "Doom's day",
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
})
|
||||
self.assertTrue(event.attendee_ids, "It should have created an attendee")
|
||||
self.assertEqual(event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
|
||||
self.assertIn(self.partner, event.message_follower_ids.partner_id, "He should be follower of the event")
|
||||
|
||||
def test_attendee_added_multi(self):
|
||||
event = self.env['calendar.event'].create({
|
||||
'name': "Doom's day",
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
})
|
||||
events = self.event | event
|
||||
events.partner_ids = self.partner
|
||||
self.assertEqual(len(events.attendee_ids), 2, "It should have created one attendee per event")
|
||||
|
||||
def test_existing_attendee_added(self):
|
||||
self.event.partner_ids = self.partner
|
||||
attendee = self.event.attendee_ids
|
||||
self.event.write({'partner_ids': [(4, self.partner.id)]}) # Add existing partner
|
||||
self.assertEqual(self.event.attendee_ids, attendee, "It should not have created an new attendee record")
|
||||
|
||||
def test_attendee_add_self(self):
|
||||
self.event.with_user(self.user).partner_ids = self.partner
|
||||
self.assertTrue(self.event.attendee_ids, "It should have created an attendee")
|
||||
self.assertEqual(self.event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
|
||||
self.assertEqual(self.event.attendee_ids.state, 'accepted', "It should be accepted for the current user")
|
||||
|
||||
def test_attendee_removed(self):
|
||||
partner_bis = self.env['res.partner'].create({'name': "Xavier"})
|
||||
self.event.partner_ids = partner_bis
|
||||
attendee = self.event.attendee_ids
|
||||
self.event.partner_ids |= self.partner
|
||||
self.event.partner_ids -= self.partner
|
||||
self.assertEqual(attendee, self.event.attendee_ids, "It should not have re-created an attendee record")
|
||||
self.assertNotIn(self.partner, self.event.attendee_ids.partner_id, "It should have removed the attendee")
|
||||
self.assertNotIn(self.partner, self.event.message_follower_ids.partner_id, "It should have unsubscribed the partner")
|
||||
self.assertIn(partner_bis, self.event.attendee_ids.partner_id, "It should have left the attendee")
|
||||
@@ -273,11 +273,9 @@ class TestCalendar(SavepointCaseWithUserDemo):
|
||||
def _test_one_mail_per_attendee(self, m, partners):
|
||||
# check that every attendee receive a (single) mail for the event
|
||||
for partner in partners:
|
||||
mail = self.env['mail.mail'].sudo().search([
|
||||
('recipient_ids', 'in', partner.id),
|
||||
('subject', 'like', m.name),
|
||||
mail = self.env['mail.message'].sudo().search([
|
||||
('notified_partner_ids', 'in', partner.id),
|
||||
])
|
||||
print(mail.mapped('subject'))
|
||||
self.assertEqual(len(mail), 1)
|
||||
|
||||
partners = [
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import patch
|
||||
from datetime import datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import SavepointCase, new_test_user
|
||||
from odoo.addons.mail.tests.common import MailCase
|
||||
|
||||
|
||||
class TestEventNotifications(SavepointCase, MailCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.event = cls.env['calendar.event'].create({
|
||||
'name': "Doom's day",
|
||||
'start': datetime(2019, 10, 25, 8, 0),
|
||||
'stop': datetime(2019, 10, 27, 18, 0),
|
||||
}).with_context(mail_notrack=True)
|
||||
cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
|
||||
cls.partner = cls.user.partner_id
|
||||
|
||||
def test_message_invite(self):
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}):
|
||||
self.event.partner_ids = self.partner
|
||||
|
||||
def test_message_invite_self(self):
|
||||
with self.assertNoNotifications():
|
||||
self.event.with_user(self.user).partner_ids = self.partner
|
||||
|
||||
def test_message_inactive_invite(self):
|
||||
self.event.active = False
|
||||
with self.assertNoNotifications():
|
||||
self.event.partner_ids = self.partner
|
||||
|
||||
def test_message_set_inactive_invite(self):
|
||||
self.event.active = False
|
||||
with self.assertNoNotifications():
|
||||
self.event.write({
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
'active': False,
|
||||
})
|
||||
|
||||
def test_message_datetime_changed(self):
|
||||
self.event.partner_ids = self.partner
|
||||
"Invitation to Presentation of the new Calendar"
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}):
|
||||
self.event.start += relativedelta(days=-1)
|
||||
|
||||
def test_message_date_changed(self):
|
||||
self.event.write({
|
||||
'allday': True,
|
||||
'start_date': date(2019, 10, 15),
|
||||
'stop_date': date(2019, 10, 15),
|
||||
})
|
||||
self.event.partner_ids = self.partner
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}):
|
||||
self.event.start_date += relativedelta(days=-1)
|
||||
|
||||
def test_message_set_inactive_date_changed(self):
|
||||
self.event.write({
|
||||
'allday': True,
|
||||
'start_date': date(2019, 10, 15),
|
||||
'stop_date': date(2019, 10, 15),
|
||||
})
|
||||
self.event.partner_ids = self.partner
|
||||
with self.assertNoNotifications():
|
||||
self.event.write({
|
||||
'start_date': self.event.start_date - relativedelta(days=1),
|
||||
'active': False,
|
||||
})
|
||||
|
||||
def test_message_inactive_date_changed(self):
|
||||
self.event.write({
|
||||
'allday': True,
|
||||
'start_date': date(2019, 10, 15),
|
||||
'stop_date': date(2019, 10, 15),
|
||||
'active': False,
|
||||
})
|
||||
self.event.partner_ids = self.partner
|
||||
with self.assertNoNotifications():
|
||||
self.event.start_date += relativedelta(days=-1)
|
||||
|
||||
def test_message_add_and_date_changed(self):
|
||||
self.event.partner_ids -= self.partner
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}):
|
||||
self.event.write({
|
||||
'start': self.event.start - relativedelta(days=1),
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
})
|
||||
|
||||
def test_bus_notif(self):
|
||||
alarm = self.env['calendar.alarm'].create({
|
||||
'name': 'Alarm',
|
||||
'alarm_type': 'notification',
|
||||
'interval': 'minutes',
|
||||
'duration': 30,
|
||||
})
|
||||
now = fields.Datetime.now()
|
||||
with patch.object(fields.Datetime, 'now', lambda: now):
|
||||
with self.assertBus([(self.env.cr.dbname, 'calendar.alarm', self.partner.id)]):
|
||||
self.event.with_context(no_mail_to_attendees=True).write({
|
||||
'start': now + relativedelta(minutes=50),
|
||||
'stop': now + relativedelta(minutes=55),
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
'alarm_ids': [(4, alarm.id)]
|
||||
})
|
||||
bus_message = [{
|
||||
"alarm_id": alarm.id,
|
||||
"event_id": self.event.id,
|
||||
"title": "Doom's day",
|
||||
"message": self.event.display_time,
|
||||
"timer": 20*60,
|
||||
"notify_at": fields.Datetime.to_string(now + relativedelta(minutes=20)),
|
||||
}]
|
||||
notif = self.env['calendar.alarm_manager'].with_user(self.user).get_next_notif()
|
||||
self.assertEqual(notif, bus_message)
|
||||
|
||||
def test_email_alarm(self):
|
||||
alarm = self.env['calendar.alarm'].create({
|
||||
'name': 'Alarm',
|
||||
'alarm_type': 'email',
|
||||
'interval': 'minutes',
|
||||
'duration': 20,
|
||||
})
|
||||
now = fields.Datetime.now()
|
||||
self.event.write({
|
||||
'start': now + relativedelta(minutes=15),
|
||||
'stop': now + relativedelta(minutes=18),
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
'alarm_ids': [(4, alarm.id)],
|
||||
})
|
||||
with patch.object(fields.Datetime, 'now', lambda: now):
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}):
|
||||
self.env['calendar.alarm_manager'].with_context(lastcall=now - relativedelta(minutes=15))._get_partner_next_mail(self.partner)
|
||||
@@ -584,7 +584,6 @@ class TestUpdateMonthlyByDay(TestRecurrentEvents):
|
||||
'stop': event.stop - relativedelta(days=4),
|
||||
})
|
||||
|
||||
# LUL TODO test shift future
|
||||
|
||||
class TestUpdateMonthlyByDate(TestRecurrentEvents):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user