From c088d5423e98ad99f1d7bfdc2e42bfac6eb418de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 20 Feb 2023 11:11:54 +0000 Subject: [PATCH] [IMP] mail: cleanup _notify_get_recipients code bits This commit contains mainly code cleaning, docstrings and a small split for notification tool methods. In this commit we * make some notification groups variable explicit; * move the filler of groups into its own submethod to ease being called from other code (to be used soon); * fix some strange overrides or code manipulation; * propagate some additional parameters to ease future commits that will improve rendering of groups-based notification emails; * cleanup, fixup and improve docstrings; This does not change anything from functional point of view, just preparing further work. Task-3046371 (Mail: Better Language Support in Composer) Part-of: odoo/odoo#106177 --- addons/crm/models/crm_lead.py | 12 +- addons/hr_holidays/models/hr_leave.py | 12 +- .../hr_holidays/models/hr_leave_allocation.py | 12 +- addons/mail/controllers/mail.py | 2 +- addons/mail/models/mail_channel.py | 6 +- addons/mail/models/mail_thread.py | 460 +++++++++++------- addons/portal/models/mail_thread.py | 10 +- addons/project/models/project.py | 11 +- addons/purchase/models/purchase.py | 6 +- addons/sale/models/sale_order.py | 6 +- addons/sms/models/mail_followers.py | 4 +- .../models/test_mail_corner_case_models.py | 6 +- addons/test_mail/models/test_mail_models.py | 12 +- addons/test_mail/tests/test_message_post.py | 11 +- .../tests/test_mail_thread_internals.py | 4 +- addons/test_mail_full/tests/test_portal.py | 4 +- addons/website_blog/models/website_blog.py | 6 +- addons/website_forum/models/forum.py | 6 +- addons/website_sale/models/sale_order.py | 6 +- addons/website_slides/models/slide_slide.py | 6 +- 20 files changed, 378 insertions(+), 224 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index ae682206165..b48ad02028f 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1924,10 +1924,12 @@ class Lead(models.Model): _('Deadline: %s', self.date_deadline.strftime(get_lang(self.env).date_format))) return render_context - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Handle salesman recipients that can convert leads into opportunities and set opportunities as won / lost. """ - groups = super(Lead, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups @@ -1955,7 +1957,11 @@ class Lead(models.Model): new_group = ( 'group_sale_salesman', lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'], - {'actions': salesman_actions} + { + 'actions': salesman_actions, + 'active': True, + 'has_button_access': True, + } ) return [new_group] + groups diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 77362ebe9b2..6c8321e3f9b 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -1648,10 +1648,12 @@ class HolidaysRequest(models.Model): return leave_notif_subtype or self.env.ref('hr_holidays.mt_leave') return super(HolidaysRequest, self)._track_subtype(init_values) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ - groups = super(HolidaysRequest, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups @@ -1670,7 +1672,11 @@ class HolidaysRequest(models.Model): new_group = ( 'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], - {'actions': hr_actions} + { + 'actions': hr_actions, + 'active': True, + 'has_button_access': True, + } ) return [new_group] + groups diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 3ae013f1c1a..d6911b2dc38 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -793,10 +793,12 @@ class HolidaysAllocation(models.Model): return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation') return super(HolidaysAllocation, self)._track_subtype(init_values) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ - groups = super(HolidaysAllocation, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups @@ -815,7 +817,11 @@ class HolidaysAllocation(models.Model): new_group = ( 'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], - {'actions': hr_actions} + { + 'actions': hr_actions, + 'active': True, + 'has_button_access': True, + } ) return [new_group] + groups diff --git a/addons/mail/controllers/mail.py b/addons/mail/controllers/mail.py index c4a4596c38d..42c2d966f2e 100644 --- a/addons/mail/controllers/mail.py +++ b/addons/mail/controllers/mail.py @@ -26,7 +26,7 @@ class MailController(http.Controller): base_link = request.httprequest.path params = dict(request.params) params.pop('token', '') - valid_token = request.env['mail.thread']._notify_encode_link(base_link, params) + valid_token = request.env['mail.thread']._encode_link(base_link, params) return consteq(valid_token, str(token)) @classmethod diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 7d858e0b9eb..85398cf38bd 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -545,12 +545,14 @@ class Channel(models.Model): return recipients_data - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ All recipients of a message on a channel are considered as partners. This means they will receive a minimal email, without a link to access in the backend. Mailing lists should indeed send minimal emails to avoid the noise. """ - groups = super(Channel, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) for (index, (group_name, _group_func, group_data)) in enumerate(groups): if group_name != 'customer': groups[index] = (group_name, lambda partner: False, group_data) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index f3f5401bc92..4c790e86607 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -2879,11 +2879,10 @@ class MailThread(models.AbstractModel): * performs the notification process by calling the various notification methods implemented; - :param message: ``mail.message`` record to notify; - :param msg_vals: dictionary of values used to create the message. If given it - may be used to access values related to ``message`` without accessing it - directly. It lessens query count in some optimized use cases by avoiding - access message content in db; + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; Kwargs allow to pass various parameters that are given to sub notification methods. See those methods for more details about supported parameters. @@ -2927,26 +2926,28 @@ class MailThread(models.AbstractModel): return recipients_data def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs): - """ Notification method: inbox. Does two main things : + """ Notificaty recipients inbox of a message. It does two main things : * create inbox notifications for users; * send bus notifications; - :param message: ``mail.message`` record to notify; - :param recipients_data: list of recipients information (based on res.partner - records), formatted like - [{'active': partner.active; - 'id': id of the res.partner being recipient to notify; - 'groups': res.group IDs if linked to a user; - 'notif': 'inbox', 'email', 'sms' (SMS App); - 'share': partner.partner_share; - 'type': 'customer', 'portal', 'user;' - }, {...}]. - See ``MailThread._notify_get_recipients``; - :param msg_vals: dictionary of values used to create the message. If given it - may be used to access values related to ``message`` without accessing it - directly. It lessens query count in some optimized use cases by avoiding - access message content in db; + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param list recipients_data: list of recipients data based on + records formatted like [ + { + 'active': partner.active; + 'id': id of the res.partner being recipient to notify; + 'is_follower': follows the message related document; + 'lang': its lang; + 'groups': res.group IDs if linked to a user; + 'notif': 'inbox', 'email', 'sms' (SMS App); + 'share': is partner a customer (partner.partner_share); + 'type': partner usage ('customer', 'portal', 'user'); + 'ushare': are users shared (if users, all users are shared); + }, {...}]. See ``MailThread._notify_get_recipients()``; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; """ bus_notifications = [] inbox_pids = [r['id'] for r in recipients_data if r['notif'] == 'inbox'] @@ -2968,39 +2969,44 @@ class MailThread(models.AbstractModel): def _notify_thread_by_email(self, message, recipients_data, msg_vals=False, mail_auto_delete=True, # mail.mail model_description=False, force_email_company=False, force_email_lang=False, # rendering + subtitles=None, # rendering resend_existing=False, force_send=True, send_after_commit=True, # email send - subtitles=None, **kwargs): - """ Method to send email linked to notified messages. + **kwargs): + """ Method to send emails notifications linked to a message. - :param message: ``mail.message`` record to notify; - :param recipients_data: list of recipients information (based on res.partner - records), formatted like - [{'active': partner.active; - 'id': id of the res.partner being recipient to notify; - 'groups': res.group IDs if linked to a user; - 'notif': 'inbox', 'email', 'sms' (SMS App); - 'share': partner.partner_share; - 'type': 'customer', 'portal', 'user;' - }, {...}]. - See ``MailThread._notify_get_recipients``; - :param msg_vals: dictionary of values used to create the message. If given it - may be used to access values related to ``message`` without accessing it - directly. It lessens query count in some optimized use cases by avoiding - access message content in db; + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param list recipients_data: list of recipients data based on + records formatted like [ + { + 'active': partner.active; + 'id': id of the res.partner being recipient to notify; + 'is_follower': follows the message related document; + 'lang': its lang; + 'groups': res.group IDs if linked to a user; + 'notif': 'inbox', 'email', 'sms' (SMS App); + 'share': is partner a customer (partner.partner_share); + 'type': partner usage ('customer', 'portal', 'user'); + 'ushare': are users shared (if users, all users are shared); + }, {...}]. See ``MailThread._notify_get_recipients()``; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; - :param mail_auto_delete: delete notification emails once sent; + :param bool mail_auto_delete: delete notification emails once sent; - :param model_description: model description used in email notification process - (computed if not given); - :param force_email_company: see ``_notify_by_email_prepare_rendering_context``; - :param force_email_lang: see ``_notify_by_email_prepare_rendering_context``; + :param str model_description: description of current model, given to + avoid fetching it and easing translation support; + :param record force_email_company: record used when rendering + notification layout. Otherwise computed based on current record; + :param str force_email_lang: lang used when rendering content, used + notably to compute model name or translate access buttons; + :param list subtitles: optional list set as template value "subtitles"; - :param resend_existing: check for existing notifications to update based on - mailed recipient, otherwise create new notifications; - :param force_send: send emails directly instead of using queue; - :param send_after_commit: if force_send, tells whether to send emails after + :param bool resend_existing: check for existing notifications to update + based on mailed recipient, otherwise create new notifications; + :param bool force_send: send emails directly instead of using queue; + :param bool send_after_commit: if force_send, tells to send emails after the transaction has been committed using a post-commit hook; - :param subtitles: optional list that will be set as template value "subtitles" """ partners_data = [r for r in recipients_data if r['notif'] == 'email'] if not partners_data: @@ -3008,9 +3014,11 @@ class MailThread(models.AbstractModel): model = msg_vals.get('model') if msg_vals else message.model model_name = model_description or (self.env['ir.model']._get(model).display_name if model else False) # one query for display name - recipients_groups_data = self._notify_get_recipients_classify(partners_data, model_name, msg_vals=msg_vals) + recipients_groups_list = self._notify_get_recipients_classify( + message, partners_data, model_name, msg_vals=msg_vals + ) - if not recipients_groups_data: + if not recipients_groups_list: return True force_send = self.env.context.get('mail_notify_force_send', force_send) @@ -3039,10 +3047,10 @@ class MailThread(models.AbstractModel): # loop on groups (customer, portal, user, ... + model specific like group_sale_salesman) notif_create_values = [] recipients_max = 50 - for recipients_group_data in recipients_groups_data: + for recipients_group in recipients_groups_list: # generate notification email content - recipients_ids = recipients_group_data.pop('recipients') - render_values = {**template_values, **recipients_group_data} + recipients_ids = recipients_group.pop('recipients') + render_values = {**template_values, **recipients_group} # {company, is_discussion, lang, message, model_description, record, record_name, signature, subtype, tracking_values, website_url} # {actions, button_access, has_button_access, recipients} @@ -3114,8 +3122,10 @@ class MailThread(models.AbstractModel): return True - def _notify_by_email_prepare_rendering_context(self, message, msg_vals=False, model_description=False, - force_email_company=False, force_email_lang=False): + def _notify_by_email_prepare_rendering_context(self, message, msg_vals=False, + model_description=False, + force_email_company=False, + force_email_lang=False): """ Prepare rendering context for notification email. Signature: if asked a default signature is computed based on author. Either @@ -3133,16 +3143,18 @@ class MailThread(models.AbstractModel): notification layout in the same language as the email content. A new parameter allows to force its value. - :param msg_vals: dictionary of values used to create the message. If given it - may be used to access values related to ``message`` without accessing it - directly. It lessens query count in some optimized use cases by avoiding - access message content in db; - :param model_description: model description used in email notification process - (computed if not given); - :param force_email_company: res.company record used when rendering notification - layout. Otherwise computed based on current record; - :param force_email_lang: lang used when rendering content, used notably to - compute model name; + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; + :param str model_description: description of current model, given to + avoid fetching it and easing translation support; + :param record force_email_company: record used when rendering + notification layout. Otherwise computed based on current record; + :param str force_email_lang: lang used when rendering content, used + notably to compute model name or translate access buttons; + + :return: dictionary of values used when rendering notification layout; """ if msg_vals is False: msg_vals = {} @@ -3226,12 +3238,15 @@ class MailThread(models.AbstractModel): } def _notify_by_email_get_base_mail_values(self, message, additional_values=None): - """ Add model-specific values to the dictionary used to create the - notification email. Its base behavior is to compute model-specific - headers. + """ Return model-specific and message-related values to be used when + creating notification emails. It serves as a common basis for all + notification emails based on a given message. - :param dict base_mail_values: base mail.mail values, holding message - to notify (mail_message_id and its fields), server, references, subject. + :param record message: record being notified; + :param dict additional_values: optional additional values to add (ease + custom calls and inheritance); + + :return: dictionary of values suitable for a create; """ mail_subject = message.subject if not mail_subject and self: @@ -3265,14 +3280,20 @@ class MailThread(models.AbstractModel): base_mail_values['headers'] = repr(headers) return base_mail_values - def _notify_by_email_get_final_mail_values(self, recipient_ids, base_mail_values, additional_values=None): - """ Format email notification recipient values to store on the notification - mail.mail. Basic method just set the recipient partners as mail_mail - recipients. Override to generate other mail values like email_to or - email_cc. - :param recipient_ids: res.partner recordset to notify + def _notify_by_email_get_final_mail_values(self, recipient_ids, mail_values, + additional_values=None): + """ Perform final formatting of values to create notification emails. + Basic method just set the recipient partners as mail_mail recipients. + Override to generate other mail values like email_to or email_cc. + + :param list recipient_ids: res.partner IDs to notify; + :param dict mail_values: notification mail values; + :param dict additional_values: optional additional values to add (ease + custom calls and inheritance); + + :return: a new dictionary of values suitable for a create; """ - final_mail_values = dict(base_mail_values) + final_mail_values = dict(mail_values) final_mail_values['recipient_ids'] = [Command.link(pid) for pid in recipient_ids] if additional_values: final_mail_values.update(additional_values) @@ -3282,8 +3303,10 @@ class MailThread(models.AbstractModel): """ Compute recipients to notify based on subtype and followers. This method returns data structured as expected for ``_notify_recipients``. - TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False - and correctly override _notify_get_recipients + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; Kwargs allow to pass various parameters that are used by sub notification methods. See those methods for more details about supported parameters. @@ -3297,15 +3320,23 @@ class MailThread(models.AbstractModel): to avoid having several notifications / partner as it would make constraints crash. This is disabled by default to optimize speed; - :return list recipients_data: this is a list of recipients information (see - ``MailFollowers._get_recipient_data()`` for more details) formatted like - [{'active': partner.active; - 'id': id of the res.partner; + TDE/XDO TODO: flag rdata directly, for example r['notif'] = 'ocn_client' + and r['needaction']=False and correctly override _notify_get_recipients + + :return list recipients_data: list of recipients information (see + ``MailFollowers._get_recipient_data()`` for more details) formatted + like [ + { + 'active': partner.active; + 'id': id of the res.partner being recipient to notify; + 'is_follower': follows the message related document; + 'lang': its lang; 'groups': res.group IDs if linked to a user; 'notif': 'inbox', 'email', 'sms' (SMS App); - 'share': partner.partner_share; - 'type': 'customer', 'portal', 'user;' - }, {...}] + 'share': is partner a customer (partner.partner_share); + 'type': partner usage ('customer', 'portal', 'user'); + 'ushare': are users shared (if users, all users are shared); + }, {...}] """ msg_sudo = message.sudo() # get values from msg_vals or from message if msg_vals doen't exists @@ -3352,133 +3383,184 @@ class MailThread(models.AbstractModel): return recipients_data - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Return groups used to classify recipients of a notification email. - Groups is a list of tuple containing of form (group_name, group_func, - group_data) where - * group_name is an identifier used only to be able to override and manipulate - groups. Default groups are user (recipients linked to an employee user), - portal (recipients linked to a portal user) and customer (recipients not - linked to any user). An example of override use would be to add a group - linked to a res.groups like Hr Officers to set specific action buttons to - them. - * group_func is a function pointer taking a partner record as parameter. This - method will be applied on recipients to know whether they belong to a given - group or not. Only first matching group is kept. Evaluation order is the - list order. - * group_data is a dict containing parameters for the notification email - * has_button_access: whether to display Access in email. True - by default for new groups, False for portal / customer. - * button_access: dict with url and title of the button - * actions: list of action buttons to display in the notification email. - Each action is a dict containing url and title of the button. - Groups has a default value that you can find in mail_thread - ``_notify_get_recipients_classify`` method. + Groups is a list of tuple (group_name, group_func, group_data) where + + * 'group_name' is an identifier used only to be able to override and + manipulate groups; + * 'group_func' is a function pointer taking a partner data dict as + parameter. It is called on recipients to know if they belong to + the group. Only first matching group is kept, iterating on the + group list in order. + * 'group_data' is a dict containing parameters used in notification + process like { + 'active': if not, it is skipped in notification process (ease + inheritance to be already present); + 'actions': list of actions to display as links or buttons in form + {'url': link of the action, 'title': link or button + string}; + 'button_access': main access document button information, {'url' + link of the access, 'title': link or button + string}; + 'has_button_access': display access document main button in email; + 'notification_group_name': name of the group, to ease usage; + 'recipients': list of partner IDs, will be fillup when evaluating + groups; + } + + Default groups: + + * 'user': recipients linked to an internal user; + * 'portal': recipients linked to a portal user; + * 'follower': recipients (not internal/portal users) follower of the + related record; + * 'customer': other recipients; + + When having to find a group for recipients, the first matching one + when iterating on groups is used. Reordering those groups is doable + through override. Adding groups is a common override, to add specific + buttons or actions for users belonging to some user groups. + + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param str model_description: description of current model, given to + avoid fetching it and easing translation support; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; + + :return: list of groups definition """ - is_thread_notification = self._notify_get_recipients_thread_info(msg_vals=msg_vals)['is_thread_notification'] return [ [ 'user', lambda pdata: pdata['type'] == 'user', - {'has_button_access': is_thread_notification} + { + 'active': True, + 'has_button_access': self._is_thread_message(msg_vals=msg_vals), + } ], [ 'portal', lambda pdata: pdata['type'] == 'portal', - {'active': False, # activate only on demand if rights are enabled - 'has_button_access': False, + { + 'active': False, # activate only on demand if rights are enabled + 'has_button_access': False, } ], [ 'follower', lambda pdata: pdata['is_follower'], - {'active': False, # activate only on demand if rights are enabled - 'has_button_access': False, + { + 'active': False, # activate only on demand if rights are enabled + 'has_button_access': False, } ], [ 'customer', lambda pdata: True, - {'has_button_access': False} + { + 'active': True, + 'has_button_access': False, + } ] ] - def _notify_get_recipients_classify(self, recipient_data, model_name, msg_vals=None): + def _notify_get_recipients_groups_fillup(self, groups, model_description, msg_vals=None): + """ Iterate on recipients groups (see '_notify_get_recipients_groups') + and fill up the result with default values, allowing to compute links or + titles once. + + :param list groups: recipients groups; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; + :param str model_description: description of current model, given to + avoid fetching it and easing translation support; + + :return: updated groups; + """ + access_link = self._notify_get_action_link('view', **msg_vals) + + if model_description: + view_title = _('View %s', model_description) + else: + view_title = _('View') + + is_thread_message = self._is_thread_message(msg_vals=msg_vals) + + # fill group_data with default_values if they are not complete + for group_name, _group_func, group_data in groups: + group_data.setdefault('active', True) + group_data.setdefault('actions', []) + group_data.setdefault('has_button_access', is_thread_message) + group_data.setdefault('notification_group_name', group_name) + group_data.setdefault('recipients', []) + group_button_access = group_data.setdefault('button_access', {}) + group_button_access.setdefault('url', access_link) + group_button_access.setdefault('title', view_title) + + return groups + + def _notify_get_recipients_classify(self, message, recipients_data, + model_description, msg_vals=None): """ Classify recipients to be notified of a message in groups to have specific rendering depending on their group. For example users could have access to buttons customers should not have in their emails. Module-specific grouping should be done by overriding ``_notify_get_recipients_groups`` method defined here-under. - :param recipient_data: list of recipients information (based on res.partner - records). See ``MailThread._notify_get_recipients()``; + :param record message: record being notified. May be + void as 'msg_vals' superseeds it; + :param list recipients_data: list of recipients data based on + records formatted like [ + { + 'active': partner.active; + 'id': id of the res.partner being recipient to notify; + 'is_follower': follows the message related document; + 'lang': its lang; + 'groups': res.group IDs if linked to a user; + 'notif': 'inbox', 'email', 'sms' (SMS App); + 'share': is partner a customer (partner.partner_share); + 'type': partner usage ('customer', 'portal', 'user'); + 'ushare': are users shared (if users, all users are shared); + }, {...}]. See ``MailThread._notify_get_recipients()``; + :param str model_description: description of current model, given to + avoid fetching it and easing translation support; + :param dict msg_vals: values dict used to create the message, allows to + skip message usage and spare some queries; - :return list: list of groups formatted for notification processing like - [{'active': True, - 'actions': [], - 'button_access': {}, - 'has_button_access': False, - 'recipients': [11],}, - {'active': True, - 'actions': [], - 'button_access': {'title': 'View Simple Chatter Model', - 'url': '/mail/view?model=mail.test.simple&res_id=1497'}, - 'has_button_access': True, - 'recipients': [4, 5, 6],}, - {'active': True, - 'actions': [], - 'button_access': {'title': 'View Simple Chatter Model', - 'url': '/mail/view?model=mail.test.simple&res_id=1497'}, - 'has_button_access': True, - 'recipients': [10, 11, 12],} - ] + :return list: list of groups (see '_notify_get_recipients_groups') + with 'recipients' key filled with matching partners, like + [{ + 'active': True, + 'actions': [], + 'button_access': {}, + 'has_button_access': False, + 'notification_group_name': 'user', + 'recipients': [11], + }, {...}] """ - # keep a local copy of msg_vals as it may be modified to include more information about groups or links + # keep a local copy of msg_vals as it may be modified to include more + # information about groups or links local_msg_vals = dict(msg_vals) if msg_vals else {} - groups = self._notify_get_recipients_groups(msg_vals=local_msg_vals) - access_link = self._notify_get_action_link('view', **local_msg_vals) - - if model_name: - view_title = _('View %s', model_name) - else: - view_title = _('View') - - # fill group_data with default_values if they are not complete - for group_name, group_func, group_data in groups: - is_thread_notification = self._notify_get_recipients_thread_info(msg_vals=local_msg_vals)['is_thread_notification'] - group_data.setdefault('active', True) - group_data.setdefault('actions', list()) - group_data.setdefault('has_button_access', is_thread_notification) - group_data.setdefault('notification_is_customer', False) - group_data.setdefault('notification_group_name', group_name) - group_data.setdefault('recipients', list()) - group_button_access = group_data.setdefault('button_access', {}) - group_button_access.setdefault('url', access_link) - group_button_access.setdefault('title', view_title) + groups = self._notify_get_recipients_groups_fillup( + self._notify_get_recipients_groups( + message, model_description, msg_vals=local_msg_vals + ), + model_description, + msg_vals=local_msg_vals + ) # classify recipients in each group - for recipient in recipient_data: - for group_name, group_func, group_data in groups: - if group_data['active'] and group_func(recipient): - group_data['recipients'].append(recipient['id']) + for recipient_data in recipients_data: + for _group_name, group_func, group_data in groups: + if group_data['active'] and group_func(recipient_data): + group_data['recipients'].append(recipient_data['id']) break # filter out groups without recipients - return [group_data for _group_name, _group_func, group_data in groups - if group_data['recipients']] - - def _notify_get_recipients_thread_info(self, msg_vals=None): - """ Tool method to compute thread info used in ``_notify_classify_recipients`` - and its sub-methods. """ - res_model = msg_vals['model'] if (msg_vals and 'model' in msg_vals) else self._name - res_id = msg_vals['res_id'] if (msg_vals and 'res_id' in msg_vals) else (self.ids[0] if self.ids else False) - return { - 'is_thread_notification': bool(res_id) if (res_model and res_model != 'mail.thread') else False - } - - @api.model - def _notify_encode_link(self, base_link, params): - secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') - token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params))) - hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest() - return hm + return [ + group_data + for _group_name, _group_func, group_data in groups + if group_data['recipients'] + ] def _notify_get_action_link(self, link_type, **kwargs): """ Prepare link to an action: view document, follow document, ... """ @@ -3507,7 +3589,7 @@ class MailThread(models.AbstractModel): return '' if link_type not in ['view']: - token = self._notify_encode_link(base_link, params) + token = self._encode_link(base_link, params) params['token'] = token link = '%s?%s' % (base_link, urls.url_encode(params, sort=True)) @@ -3516,6 +3598,24 @@ class MailThread(models.AbstractModel): return link + @api.model + def _encode_link(self, base_link, params): + secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') + token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params))) + hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest() + return hm + + def _is_thread_message(self, msg_vals=None): + """ Tool method to compute thread validity in notification methods. + msg_vals is used as a replacement for self, allowing to force model + and res_id independently of current recordset. Void values in dict + are kept e.g. model=False is valid. """ + if msg_vals is None: + msg_vals = {} + res_model = msg_vals['model'] if 'model' in msg_vals else self._name + res_id = msg_vals['res_id'] if 'res_id' in msg_vals else (self.ids[0] if self.ids else False) + return bool(res_id) if (res_model and res_model != 'mail.thread') else False + # ------------------------------------------------------ # FOLLOWERS API # ------------------------------------------------------ diff --git a/addons/portal/models/mail_thread.py b/addons/portal/models/mail_thread.py index 786fb02bf32..6f020cb6bd7 100644 --- a/addons/portal/models/mail_thread.py +++ b/addons/portal/models/mail_thread.py @@ -16,8 +16,10 @@ class MailThread(models.AbstractModel): domain=lambda self: [('model', '=', self._name), '|', ('message_type', '=', 'comment'), ('message_type', '=', 'email')], auto_join=True, help="Website communication history") - def _notify_get_recipients_groups(self, msg_vals=None): - groups = super()._notify_get_recipients_groups(msg_vals=msg_vals) + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups @@ -37,11 +39,11 @@ class MailThread(models.AbstractModel): new_group = [ ('portal_customer', lambda pdata: pdata['id'] == customer.id, { - 'has_button_access': True, + 'active': True, 'button_access': { 'url': access_link, }, - 'notification_is_customer': True, + 'has_button_access': True, }) ] else: diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 13d4aac5049..4d9b2a06225 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -2440,12 +2440,14 @@ class Task(models.Model): res -= waiting_subtype return res - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Handle project users and managers recipients that can assign tasks and create new one directly from notification emails. Also give access button to portal users and portal customers. If they are notified they should probably have access to the document. """ - groups = super(Task, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups @@ -2464,7 +2466,10 @@ class Task(models.Model): groups.insert(0, ( 'allowed_portal_users', lambda pdata: pdata['type'] == 'portal', - {} + { + 'active': True, + 'has_button_access': True, + } )) portal_privacy = self.project_id.privacy_visibility == 'portal' for group_name, _group_method, group_data in groups: diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index a19c0513228..bfeecdfcf3e 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -341,10 +341,12 @@ class PurchaseOrder(models.Model): kwargs['notify_author'] = self.env.user.partner_id.id in (kwargs.get('partner_ids') or []) return super(PurchaseOrder, self.with_context(**po_ctx)).message_post(**kwargs) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Tweak 'view document' button for portal customers, calling directly routes for confirm specific to PO model. """ - groups = super(PurchaseOrder, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 1ecd1231ed6..8c05e855459 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1249,11 +1249,13 @@ class SaleOrder(models.Model): kwargs['notify_author'] = self.env.user.partner_id.id in (kwargs.get('partner_ids') or []) return super(SaleOrder, self.with_context(**so_ctx)).message_post(**kwargs) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Give access button to users and portal customer as portal is integrated in sale. Customer and portal group have probably no right to see the document so they don't have the access button. """ - groups = super()._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups diff --git a/addons/sms/models/mail_followers.py b/addons/sms/models/mail_followers.py index 248da8afd1d..eb72da59d95 100644 --- a/addons/sms/models/mail_followers.py +++ b/addons/sms/models/mail_followers.py @@ -8,8 +8,9 @@ class Followers(models.Model): _inherit = ['mail.followers'] def _get_recipient_data(self, records, message_type, subtype_id, pids=None): + recipients_data = super()._get_recipient_data(records, message_type, subtype_id, pids=pids) if message_type != 'sms' or not (pids or records): - return super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids) + return recipients_data if pids is None and records: records_pids = dict( @@ -20,7 +21,6 @@ class Followers(models.Model): records_pids = dict((record.id, pids) for record in records) else: records_pids = {0: pids if pids else []} - recipients_data = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids) for rid, rdata in recipients_data.items(): sms_pids = records_pids.get(rid) or [] for pid, pdata in rdata.items(): diff --git a/addons/test_mail/models/test_mail_corner_case_models.py b/addons/test_mail/models/test_mail_corner_case_models.py index 18428e2432b..6bf08989f70 100644 --- a/addons/test_mail/models/test_mail_corner_case_models.py +++ b/addons/test_mail/models/test_mail_corner_case_models.py @@ -72,8 +72,10 @@ class MailTestLang(models.Model): def _mail_get_partner_fields(self): return ['customer_id'] - def _notify_get_recipients_groups(self, msg_vals=None): - groups = super(MailTestLang, self)._notify_get_recipients_groups(msg_vals=msg_vals) + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) local_msg_vals = dict(msg_vals or {}) diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index 460faaf4584..869da9f78ce 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -160,11 +160,13 @@ class MailTestTicket(models.Model): for record in self ) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Activate more groups to test query counters notably (and be backward compatible for tests). """ local_msg_vals = dict(msg_vals or {}) - groups = super()._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) for group_name, _group_method, group_data in groups: if group_name == 'portal': group_data['active'] = True @@ -311,10 +313,12 @@ class MailTestContainer(models.Model): for record in self ) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Activate more groups to test query counters notably (and be backward compatible for tests). """ - groups = super(MailTestContainer, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) for group_name, _group_method, group_data in groups: if group_name == 'portal': group_data['active'] = True diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 929bd264130..d43ec23821d 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -222,7 +222,8 @@ class TestMailNotifyAPI(TestMessagePostCommon): # test notifying the class (void recordset) classify_res = self.env[base_record._name]._notify_get_recipients_classify( - pdata, 'My Custom Model Name', msg_vals=notify_msg_vals + self.env['mail.message'], pdata, 'My Custom Model Name', + msg_vals=notify_msg_vals, ) # find back information for each recipients partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) @@ -249,7 +250,9 @@ class TestMailNotifyAPI(TestMessagePostCommon): 'res_id': res_id, }) classify_res = self.env[model].browse(res_id)._notify_get_recipients_classify( - pdata, 'Test', msg_vals=notify_msg_vals) + self.env['mail.message'], pdata, 'Test', + msg_vals=notify_msg_vals, + ) # find back information for partner partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids) @@ -271,7 +274,9 @@ class TestMailNotifyAPI(TestMessagePostCommon): 'res_id': res_id, }) classify_res = base_record._notify_get_recipients_classify( - pdata, 'Test', msg_vals=notify_msg_vals) + self.env['mail.message'], pdata, 'Test', + msg_vals=notify_msg_vals, + ) # find back information for partner partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids) diff --git a/addons/test_mail_full/tests/test_mail_thread_internals.py b/addons/test_mail_full/tests/test_mail_thread_internals.py index d921b8d9d74..d7423e61916 100644 --- a/addons/test_mail_full/tests/test_mail_thread_internals.py +++ b/addons/test_mail_full/tests/test_mail_thread_internals.py @@ -56,7 +56,9 @@ class TestMailThreadInternals(TestMailThreadInternalsCommon): 'By default access tokens are False with portal' ) - groups = test_record._notify_get_recipients_groups() + groups = test_record._notify_get_recipients_groups( + self.env['mail.message'], False, + ) portal_customer_group = next( (group for group in groups if group[0] == 'portal_customer'), False diff --git a/addons/test_mail_full/tests/test_portal.py b/addons/test_mail_full/tests/test_portal.py index 08e7503ebe0..09c7492359e 100644 --- a/addons/test_mail_full/tests/test_portal.py +++ b/addons/test_mail_full/tests/test_portal.py @@ -182,7 +182,9 @@ class TestPortalFlow(MailCommon, HttpCase): 'subject': 'Your quotation "{{ object.name }}"', }) cls._create_portal_user() - for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups(): + for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups( + cls.env['mail.message'], False + ): if group_name == 'portal_customer' and group_func(cls.customer): cls.record_access_url = group_data['button_access']['url'] break diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 1089daab36e..624515f5aa6 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -274,9 +274,11 @@ class BlogPost(models.Model): 'res_id': self.id, } - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Add access button to everyone if the document is published. """ - groups = super(BlogPost, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index d6e3c0a3475..d33ca5a77ea 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -934,9 +934,11 @@ class Post(models.Model): 'res_id': self.id, } - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Add access button to everyone if the document is active. """ - groups = super(Post, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 629f7817c2c..50ae8c4bcbe 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -402,10 +402,12 @@ class SaleOrder(models.Model): self.cart_recovery_email_sent = True return super(SaleOrder, self)._message_post_after_hook(message, msg_vals) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ In case of cart recovery email, update link to redirect directly to the cart (like ``mail_template_sale_cart_recovery`` template). """ - groups = super(SaleOrder, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 67b48c9028e..85b8595a064 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -735,9 +735,11 @@ class Slide(models.Model): } return super(Slide, self)._get_access_action(access_uid=access_uid, force_website=force_website) - def _notify_get_recipients_groups(self, msg_vals=None): + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): """ Add access button to everyone if the document is active. """ - groups = super(Slide, self)._notify_get_recipients_groups(msg_vals=msg_vals) + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) if not self: return groups