From 86bf50db05a1bd04a27e03a15d6ce05c7609162f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 2 Mar 2018 12:59:33 +0100 Subject: [PATCH 1/6] [IMP] mail: introduce a new computation for subscribing followers This commit adds new methods to add or update followers to documents. Purpose is to have new optimized methods that perform less queries. It is based on experience and use cases of various addons in Odoo using it. Technically two main methods are given on mail.followers model * _add_followers: this method generates values required to create new followers or update existing one. It takes several parameter controlling how to deal with existing followers. This is used notably when auto subscription should not erase existing subscriptions; * _insert_followers: this method creates or updates followers for a given set of documents. It uses result from _add_followers; Performance gain come from the use of a SQL query to fetch followers data as well as less use of record sets. Code can be more low-level in some places. Purpose is to avoid browse and prefetch and just fetch data once and use it in various computation. New implementation is equivalent to the old one. It offers more flexibility as caller can either receive values for followers or directly have records created and updated. Various parameter allow to tune computation depending on each specific call to the new follower methods. Future commits will gradually replace the use of old _add_follower_command by the new methods. Purpose is to replace them one by one in order to see the gain in term of performance one replacement at a time. --- addons/mail/models/mail_followers.py | 174 +++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index 281e6f1c5b4..270f2404251 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import itertools + from odoo import api, fields, models @@ -145,3 +147,175 @@ class Followers(models.Model): ('mail_followers_res_channel_res_model_id_uniq', 'unique(res_model,res_id,channel_id)', 'Error, a channel cannot follow twice the same object.'), ('partner_xor_channel', 'CHECK((partner_id IS NULL) != (channel_id IS NULL))', 'Error: A follower must be either a partner or a channel (but not both).') ] + + # -------------------------------------------------- + # Private tools methods to fetch followers data + # -------------------------------------------------- + + def _get_subscription_data(self, doc_data, pids, cids, include_pshare=False): + """ Private method allowing to fetch follower data from several documents of a given model. + Followers can be filtered given partner IDs and channel IDs. + + :param doc_data: list of pair (res_model, res_ids) that are the documents from which we + want to have subscription data; + :param pids: optional partner to filter; if None take all, otherwise limitate to pids + :param cids: optional channel to filter; if None take all, otherwise limitate to cids + :param include_pshare: optional join in partner to fetch their share status + + :return: list of followers data which is a list of tuples containing + follower ID, + document ID, + partner ID (void if channel_id), + channel ID (void if partner_id), + followed subtype IDs, + share status of partner (void id channel_id, returned only if include_pshare is True) + """ + # base query: fetch followers of given documents + where_clause = ' OR '.join(['fol.res_model = %s AND fol.res_id IN %s'] * len(doc_data)) + where_params = list(itertools.chain.from_iterable((rm, tuple(rids)) for rm, rids in doc_data)) + + # additional: filter on optional pids / cids + sub_where = [] + if pids: + sub_where += ["fol.partner_id IN %s"] + where_params.append(tuple(pids)) + elif pids is not None: + sub_where += ["fol.partner_id IS NULL"] + if cids: + sub_where += ["fol.channel_id IN %s"] + where_params.append(tuple(cids)) + elif cids is not None: + sub_where += ["fol.channel_id IS NULL"] + if sub_where: + where_clause += "AND (%s)" % " OR ".join(sub_where) + + query = """ +SELECT fol.id, fol.res_id, fol.partner_id, fol.channel_id, array_agg(subtype.id)%s +FROM mail_followers fol +%s +LEFT JOIN mail_followers_mail_message_subtype_rel fol_rel ON fol_rel.mail_followers_id = fol.id +LEFT JOIN mail_message_subtype subtype ON subtype.id = fol_rel.mail_message_subtype_id +WHERE %s +GROUP BY fol.id%s""" % ( + ', partner.partner_share' if include_pshare else '', + 'LEFT JOIN res_partner partner ON partner.id = fol.partner_id' if include_pshare else '', + where_clause, + ', partner.partner_share' if include_pshare else '') + self.env.cr.execute(query, tuple(where_params)) + return self.env.cr.fetchall() + + # -------------------------------------------------- + # Private tools methods to generate new subscription + # -------------------------------------------------- + + def _insert_followers(self, res_model, res_ids, partner_ids, partner_subtypes, channel_ids, channel_subtypes, + customer_ids=None, check_existing=False, existing_policy='skip'): + """ Main internal method allowing to create or update followers for documents, given a + res_model and the document res_ids. This method does not handle access rights. This is the + role of the caller to ensure there is no security breach. + + :param partner_subtypes: optional subtypes for new partner followers. If not given, default + ones are computed; + :param channel_subtypes: optional subtypes for new channel followers. If not given, default + ones are computed; + :param customer_ids: see ``_add_default_followers`` + :param check_existing: see ``_add_followers``; + :param existing_policy: see ``_add_followers``; + """ + sudo_self = self.sudo() + if not partner_subtypes and not channel_subtypes: # no subtypes -> default computation, no force, skip existing + new, upd = self._add_default_followers(res_model, res_ids, partner_ids, channel_ids, customer_ids=customer_ids) + else: + new, upd = self._add_followers(res_model, res_ids, partner_ids, partner_subtypes, channel_ids, channel_subtypes, check_existing=check_existing, existing_policy=existing_policy) + for res_id, values_list in new.items(): + for values in values_list: + sudo_self.create(dict(values, res_id=res_id)).id + for fol_id, values in upd.items(): + sudo_self.browse(fol_id).write(values) + + def _add_default_followers(self, res_model, res_ids, partner_ids, channel_ids=None, customer_ids=None): + """ Shortcut to ``_add_followers`` that computes default subtypes. Existing + followers are skipped as their subscription is considered as more important + compared to new default subscription. + + :param customer_ids: optional list of partner ids that are customers. It is used if computing + default subtype is necessary and allow to avoid the check of partners being customers (no + user or share user). It is just a matter of saving queries if the info is already known; + + :return: see ``_add_followers`` + """ + if not partner_ids and not channel_ids: + return dict(), dict() + + default, _, external = self.env['mail.message.subtype'].default_subtypes(res_model) + if partner_ids and customer_ids is None: + customer_ids = self.env['res.partner'].sudo().search([('id', 'in', partner_ids), ('partner_share', '=', True)]).ids + + c_stypes = dict.fromkeys(channel_ids or [], default.ids) + p_stypes = dict((pid, external.ids if pid in customer_ids else default.ids) for pid in partner_ids) + + return self._add_followers(res_model, res_ids, partner_ids, p_stypes, channel_ids, c_stypes, check_existing=True, existing_policy='skip') + + def _add_followers(self, res_model, res_ids, partner_ids, partner_subtypes, channel_ids, channel_subtypes, + check_existing=False, existing_policy='skip'): + """ Internal method that generates values to insert or update followers. Callers have to + handle the result, for example by making a valid ORM command, inserting or updating directly + follower records, ... This method returns two main data + + * first one is a dict which keys are res_ids. Value is a list of dict of values valid for + creating new followers for the related res_id; + * second one is a dict which keys are follower ids. Value is a dict of values valid for + updating the related follower record; + + :param check_existing: if True, check for existing followers for given documents and handle + them according to existing_policy parameter. Setting to False allows to save some computation + if caller is sure there are no conflict for followers; + :param existing policy: if check_existing, tells what to do with already-existing followers: + + * skip: simply skip existing followers, do not touch them; + * force: update existing with given subtypes only; + * update: gives an update dict allowing to add missing subtypes (no subtype removal); + """ + _res_ids = res_ids or [0] + data_fols, doc_pids, doc_cids = dict(), dict((i, set()) for i in _res_ids), dict((i, set()) for i in _res_ids) + + if check_existing and res_ids: + for fid, rid, pid, cid, sids in self._get_subscription_data([(res_model, res_ids)], partner_ids or None, channel_ids or None): + if existing_policy != 'force': + if pid: + doc_pids[rid].add(pid) + elif cid: + doc_cids[rid].add(cid) + data_fols[fid] = (rid, pid, cid, sids) + + if existing_policy == 'force': + self.sudo().browse(data_fols.keys()).unlink() + + new, update = dict(), dict() + for res_id in _res_ids: + for partner_id in set(partner_ids or []): + if partner_id not in doc_pids[res_id]: + new.setdefault(res_id, list()).append({ + 'res_model': res_model, + 'partner_id': partner_id, + 'subtype_ids': [(6, 0, partner_subtypes[partner_id])], + }) + elif existing_policy == 'update': + fol_id, sids = next(((key, val[3]) for key, val in data_fols.items() if val[0] == res_id and val[1] == partner_id), (False, [])) + new_sids = set(partner_subtypes[partner_id]) - set(sids) + if fol_id and new_sids: + update[fol_id] = {'subtype_ids': [(4, sid) for sid in new_sids]} + for channel_id in set(channel_ids or []): + if channel_id not in doc_cids[res_id]: + new.setdefault(res_id, list()).append({ + 'res_model': res_model, + 'channel_id': channel_id, + 'subtype_ids': [(6, 0, channel_subtypes[channel_id])], + }) + elif existing_policy == 'update': + fol_id, sids = next(((key, val[3]) for key, val in data_fols.items() if val[0] == res_id and val[2] == channel_id), (False, [])) + new_sids = set(channel_subtypes[channel_id]) - set(sids) + if fol_id and new_sids: + update[fol_id] = {'subtype_ids': [(4, sid) for sid in new_sids]} + + return new, update From 301f140cb6c872e3f4303f6ce46c626d3f04fa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 2 Mar 2018 14:21:08 +0100 Subject: [PATCH 2/6] [REF] mail: use new followers command when subscribing record creator Override of create in mail.thread now uses the recently introduced _add_default_followers to add the record's creator in the followers. It allows to save one query when creating new records. It means for simple models we approach the bottom limit in term of generated queries which is quite a good news. --- addons/mail/models/mail_thread.py | 4 ++-- addons/test_mail/tests/test_performance.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 1b52ec94e5d..d58b6d7dd33 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -219,8 +219,8 @@ class MailThread(models.AbstractModel): # subscribe uid unless asked not to if not self._context.get('mail_create_nosubscribe'): - message_follower_ids = values.get('message_follower_ids') or [] # webclient can send None or False - message_follower_ids += self.env['mail.followers']._add_follower_command(self._name, [], {self.env.user.partner_id.id: None}, {}, force=True)[0] + message_follower_ids = values.get('message_follower_ids') or [] + message_follower_ids += [(0, 0, fol_vals) for fol_vals in self.env['mail.followers']._add_default_followers(self._name, [], self.env.user.partner_id.ids, customer_ids=[])[0][0]] values['message_follower_ids'] = message_follower_ids thread = super(MailThread, self).create(values) diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index b8e3b71d249..feca36fde86 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -93,7 +93,7 @@ class TestMailPerformance(TransactionCase): @warmup def test_create_mail_with_tracking(self): """ Create records inheriting from 'mail.thread' (with field tracking). """ - with self.assertQueryCount(admin=16, demo=16): # test_mail only: 16 - 16 + with self.assertQueryCount(admin=15, demo=15): # test_mail only: 15 - 15 self.env['test_performance.mail'].create({'name': 'X'}) @users('admin', 'emp') @@ -148,7 +148,7 @@ class TestAdvMailPerformance(TransactionCase): def test_adv_activity(self): model = self.env['mail.test.activity'] - with self.assertQueryCount(admin=9, emp=9): # test_mail only: 9 - 9 + with self.assertQueryCount(admin=8, emp=8): # test_mail only: 8 - 8 model.create({'name': 'Test'}) @users('admin', 'emp') @@ -494,7 +494,7 @@ class TestHeavyMailPerformance(TransactionCase): customer_id = self.customer.id user_id = self.user_portal.id - with self.assertQueryCount(admin=319, emp=380): # com runbot: 319 - 179 // test_mail only: 312 - 373 + with self.assertQueryCount(admin=318, emp=379): # com runbot: 318 - 378 // test_mail only: 311 - 372 rec = self.env['mail.test.full'].create({ 'name': 'Test', 'umbrella_id': umbrella_id, From b6d2351df45bdd049e42e828743b5dd2f1941649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 2 Mar 2018 17:48:32 +0100 Subject: [PATCH 3/6] [REF] mail: improve implementation of message subscribe This commit refactors mail.thread message_subscribe method. It is done for two purposes. First one is to optimize performance by using the new followers computation methods introduced recently. Second purpose is to clean the API of message_subscribe to make it simpler to use. This commit splits message_subscribe in two main parts : * _message_subscribe is a private method calling the follower new subscription methods and updating the record set; * message_subscribe is a public wrapper on _message_subscribe that adds access rights checks; Simplification comes by removing message_subscribe_users that was a shortcut to message_subscribe. Having a method to subscribe partners and channels is sufficient as we would like to avoid bloating the public API of mail.thread. A force parameter is also removed from message_subscribe as this implementation detail can be induced in the computation. Various addons using the removed methods are updated in order to use the new subscription API. They have the same functional behavior. This commit has a great impact when subscribing several followers. In a more general way all code using message_post is also optimized as posting a message generally implies subscribing followers. It also improves activity use as posting a message and subscribing new followers are common process in activities. --- addons/calendar/models/calendar.py | 13 +-- .../crm/tests/test_new_lead_notification.py | 3 +- addons/hr/models/hr.py | 4 +- addons/hr_expense/models/hr_expense.py | 2 +- addons/hr_holidays/models/hr_leave.py | 2 +- .../hr_holidays/models/hr_leave_allocation.py | 2 +- addons/hr_maintenance/models/equipment.py | 24 +++--- addons/mail/models/ir_actions.py | 2 +- addons/mail/models/mail_thread.py | 79 ++++++++++--------- addons/maintenance/models/maintenance.py | 8 +- addons/project/models/project.py | 16 ++-- addons/project/tests/test_access_rights.py | 4 +- addons/project/tests/test_portal.py | 10 +-- addons/test_mail/tests/test_mail_followers.py | 6 +- .../test_mail/tests/test_message_compose.py | 4 +- addons/test_mail/tests/test_performance.py | 50 ++++++------ .../website_event_track/controllers/main.py | 2 +- addons/website_slides/models/slides.py | 2 +- 18 files changed, 117 insertions(+), 116 deletions(-) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 7e5ee7c18f8..2fe06683269 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1396,13 +1396,14 @@ class Meeting(models.Model): return super(Meeting, self.browse(thread_id)).message_post(**kwargs) @api.multi - def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, force=True): + def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): records = self.browse(get_real_ids(self.ids)) - return super(Meeting, records).message_subscribe( - partner_ids=partner_ids, - channel_ids=channel_ids, - subtype_ids=subtype_ids, - force=force) + return super(Meeting, records).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids) + + @api.multi + def _message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, customer_ids=None): + records = self.browse(get_real_ids(self.ids)) + return super(Meeting, records)._message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids, customer_ids=customer_ids) @api.multi def message_unsubscribe(self, partner_ids=None, channel_ids=None): diff --git a/addons/crm/tests/test_new_lead_notification.py b/addons/crm/tests/test_new_lead_notification.py index 20f958e25c7..7c9b6d151a3 100644 --- a/addons/crm/tests/test_new_lead_notification.py +++ b/addons/crm/tests/test_new_lead_notification.py @@ -17,8 +17,7 @@ class NewLeadNotification(TestCrmCases): }) subtype = self.env.ref("crm.mt_salesteam_lead") - sales_team_1.message_subscribe(partner_ids=[self.crm_salesman.partner_id.id], subtype_ids=[subtype.id]) - sales_team_1.message_subscribe(channel_ids=[channel_listen.id], subtype_ids=[subtype.id]) + sales_team_1.message_subscribe(partner_ids=[self.crm_salesman.partner_id.id], channel_ids=[channel_listen.id], subtype_ids=[subtype.id]) # Imitate what happens in the controller when somebody creates a new # lead from the website form diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index 60fa38e7b14..eeec14e12cf 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -286,7 +286,7 @@ class Department(models.Model): department = super(Department, self.with_context(mail_create_nosubscribe=True)).create(vals) manager = self.env['hr.employee'].browse(vals.get("manager_id")) if manager.user_id: - department.message_subscribe_users(user_ids=manager.user_id.ids) + department.message_subscribe(partner_ids=manager.user_id.partner_id.ids) return department @api.multi @@ -303,7 +303,7 @@ class Department(models.Model): manager = self.env['hr.employee'].browse(manager_id) # subscribe the manager user if manager.user_id: - self.message_subscribe_users(user_ids=manager.user_id.ids) + self.message_subscribe(partner_ids=manager.user_id.partner_id.ids) # set the employees's parent to the new manager self._update_employee_manager(manager_id) return super(Department, self).write(vals) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index f021eb4d10f..5519ca8a888 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -574,7 +574,7 @@ class HrExpenseSheet(models.Model): def _add_followers(self): users = self._get_users_to_subscribe() - self.message_subscribe_users(user_ids=users.ids) + self.message_subscribe(partner_ids=users.mapped('partner_id').ids) @api.model def _create_set_followers(self, values): diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 03c90176363..5a79fef7c54 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -223,7 +223,7 @@ class HolidaysRequest(models.Model): def add_follower(self, employee_id): employee = self.env['hr.employee'].browse(employee_id) if employee.user_id: - self.message_subscribe_users(user_ids=employee.user_id.ids) + self.message_subscribe(partner_ids=employee.user_id.partner_id.ids) @api.multi @api.constrains('holiday_status_id') diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index fd98213e091..45b0057d889 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -124,7 +124,7 @@ class HolidaysAllocation(models.Model): def add_follower(self, employee_id): employee = self.env['hr.employee'].browse(employee_id) if employee.user_id: - self.message_subscribe_users(user_ids=employee.user_id.ids) + self.message_subscribe(partner_ids=employee.user_id.partner_id.ids) @api.multi @api.constrains('holiday_status_id') diff --git a/addons/hr_maintenance/models/equipment.py b/addons/hr_maintenance/models/equipment.py index 717275d89a9..ebfa6eb03fe 100644 --- a/addons/hr_maintenance/models/equipment.py +++ b/addons/hr_maintenance/models/equipment.py @@ -36,29 +36,29 @@ class MaintenanceEquipment(models.Model): def create(self, vals): equipment = super(MaintenanceEquipment, self).create(vals) # subscribe employee or department manager when equipment assign to him. - user_ids = [] + partner_ids = [] if equipment.employee_id and equipment.employee_id.user_id: - user_ids.append(equipment.employee_id.user_id.id) + partner_ids.append(equipment.employee_id.user_id.partner_id.id) if equipment.department_id and equipment.department_id.manager_id and equipment.department_id.manager_id.user_id: - user_ids.append(equipment.department_id.manager_id.user_id.id) - if user_ids: - equipment.message_subscribe_users(user_ids=user_ids) + partner_ids.append(equipment.department_id.manager_id.user_id.partner_id.id) + if partner_ids: + equipment.message_subscribe(partner_ids=partner_ids) return equipment @api.multi def write(self, vals): - user_ids = [] + partner_ids = [] # subscribe employee or department manager when equipment assign to employee or department. if vals.get('employee_id'): user_id = self.env['hr.employee'].browse(vals['employee_id'])['user_id'] if user_id: - user_ids.append(user_id.id) + partner_ids.append(user_id.partner_id.id) if vals.get('department_id'): department = self.env['hr.department'].browse(vals['department_id']) if department and department.manager_id and department.manager_id.user_id: - user_ids.append(department.manager_id.user_id.id) - if user_ids: - self.message_subscribe_users(user_ids=user_ids) + partner_ids.append(department.manager_id.user_id.partner_id.id) + if partner_ids: + self.message_subscribe(partner_ids=partner_ids) return super(MaintenanceEquipment, self).write(vals) @api.multi @@ -105,7 +105,7 @@ class MaintenanceRequest(models.Model): def create(self, vals): result = super(MaintenanceRequest, self).create(vals) if result.employee_id.user_id: - result.message_subscribe_users(user_ids=[result.employee_id.user_id.id]) + result.message_subscribe(partner_ids=[result.employee_id.user_id.partner_id.id]) return result @api.multi @@ -113,7 +113,7 @@ class MaintenanceRequest(models.Model): if vals.get('employee_id'): employee = self.env['hr.employee'].browse(vals['employee_id']) if employee and employee.user_id: - self.message_subscribe_users(user_ids=[employee.user_id.id]) + self.message_subscribe(partner_ids=[employee.user_id.partner_id.id]) return super(MaintenanceRequest, self).write(vals) @api.model diff --git a/addons/mail/models/ir_actions.py b/addons/mail/models/ir_actions.py index a8faa2703b8..bd4b03f3a4c 100644 --- a/addons/mail/models/ir_actions.py +++ b/addons/mail/models/ir_actions.py @@ -40,7 +40,7 @@ class ServerActions(models.Model): Model = self.env[action.model_id.model] if self.partner_ids or self.channel_ids and hasattr(Model, 'message_subscribe'): records = Model.browse(self._context.get('active_ids', self._context.get('active_id'))) - records.message_subscribe(self.partner_ids.ids, self.channel_ids.ids, force=False) + records.message_subscribe(self.partner_ids.ids, self.channel_ids.ids) return False @api.model diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index d58b6d7dd33..6891e016ced 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1849,7 +1849,7 @@ class MailThread(models.AbstractModel): partner_to_subscribe = partner_ids if self._context.get('mail_post_autofollow_partner_ids'): partner_to_subscribe = [p for p in partner_ids if p in self._context.get('mail_post_autofollow_partner_ids')] - self.message_subscribe(list(partner_to_subscribe), force=False) + self.message_subscribe(list(partner_to_subscribe)) # _mail_flat_thread: automatically set free messages to the first posted message MailMessage = self.env['mail.message'] @@ -1908,7 +1908,7 @@ class MailThread(models.AbstractModel): # Post-process: subscribe author if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'): - self.message_subscribe([values['author_id']], force=False) + self._message_subscribe([values['author_id']]) @api.multi def message_post_with_view(self, views_or_xmlid, **kwargs): @@ -2034,26 +2034,18 @@ class MailThread(models.AbstractModel): # ------------------------------------------------------ @api.multi - def message_subscribe_users(self, user_ids=None, subtype_ids=None): - """ Wrapper on message_subscribe, using users. If user_ids is not - provided, subscribe uid instead. """ - if user_ids is None: - user_ids = [self._uid] - return self.message_subscribe(self.env['res.users'].browse(user_ids).mapped('partner_id').ids, subtype_ids=subtype_ids) - - @api.multi - def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, force=True): - """ Add partners to the records followers. """ - # not necessary for computation, but saves an access right check - if not partner_ids and not channel_ids: + def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): + """ Main public API to add followers to a record set. Its main purpose is + to perform access rights checks before calling ``_message_subscribe``. """ + if not self or (not partner_ids and not channel_ids): return True - if partner_ids is None: - partner_ids = [] - if channel_ids is None: - channel_ids = [] - # TDE CHECK THIS - if not channel_ids and partner_ids and set(partner_ids) == set([self.env.user.partner_id.id]): + partner_ids = partner_ids or [] + channel_ids = channel_ids or [] + adding_current = set(partner_ids) == set([self.env.user.partner_id.id]) + customer_ids = [] if adding_current else None + + if not channel_ids and partner_ids and adding_current: try: self.check_access_rights('read') self.check_access_rule('read') @@ -2063,25 +2055,38 @@ class MailThread(models.AbstractModel): self.check_access_rights('write') self.check_access_rule('write') - partner_data = dict((pid, subtype_ids) for pid in partner_ids) - channel_data = dict((cid, subtype_ids) for cid in channel_ids) - gen, part = self.env['mail.followers']._add_follower_command(self._name, self.ids, partner_data, channel_data, force=force) - self.sudo().write({'message_follower_ids': gen}) - for record in self.filtered(lambda self: self.id in part): - record.write({'message_follower_ids': part[record.id]}) + return self._message_subscribe(partner_ids, channel_ids, subtype_ids, customer_ids=customer_ids) + + def _message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, customer_ids=None): + """ Main private API to add followers to a record set. This method adds + partners and channels, given their IDs, as followers of all records + contained in the record set. + + If subtypes are given existing followers are erased with new subtypes. + If default one have to be computed only missing followers will be added + with default subtypes matching the record set model. + + This private method does not specifically check for access right. Use + ``message_subscribe`` public API when not sure about access rights. + + :param customer_ids: see ``_insert_followers`` """ + if not self: + return True + + if not subtype_ids: + self.env['mail.followers']._insert_followers( + self._name, self.ids, partner_ids, None, channel_ids, None, + customer_ids=customer_ids) + else: + self.env['mail.followers']._insert_followers( + self._name, self.ids, + partner_ids, dict((pid, subtype_ids) for pid in partner_ids), + channel_ids, dict((cid, subtype_ids) for cid in channel_ids), + customer_ids=customer_ids, check_existing=True, existing_policy='force') self.invalidate_cache() return True - @api.multi - def message_unsubscribe_users(self, user_ids=None): - """ Wrapper on message_subscribe, using users. If user_ids is not - provided, unsubscribe uid instead. """ - if user_ids is None: - user_ids = [self._uid] - partner_ids = [user.partner_id.id for user in self.env['res.users'].browse(user_ids)] - return self.message_unsubscribe(partner_ids) - @api.multi def message_unsubscribe(self, partner_ids=None, channel_ids=None): """ Remove partners from the records followers. """ @@ -2224,10 +2229,10 @@ class MailThread(models.AbstractModel): for pid, subtypes in new_partners.items(): subtypes = list(subtypes) if subtypes is not None else None - self.message_subscribe(partner_ids=[pid], subtype_ids=subtypes, force=(subtypes != None)) + self.message_subscribe(partner_ids=[pid], subtype_ids=subtypes) for cid, subtypes in new_channels.items(): subtypes = list(subtypes) if subtypes is not None else None - self.message_subscribe(channel_ids=[cid], subtype_ids=subtypes, force=(subtypes != None)) + self.message_subscribe(channel_ids=[cid], subtype_ids=subtypes) # remove the current user from the needaction partner to avoid to notify the author of the message user_pids = [user.partner_id.id for user in to_add_users if user != self.env.user] diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index e324123595f..6f4e02b4814 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -208,13 +208,13 @@ class MaintenanceEquipment(models.Model): def create(self, vals): equipment = super(MaintenanceEquipment, self).create(vals) if equipment.owner_user_id: - equipment.message_subscribe_users(user_ids=[equipment.owner_user_id.id]) + equipment.message_subscribe(partner_ids=[equipment.owner_user_id.partner_id.id]) return equipment @api.multi def write(self, vals): if vals.get('owner_user_id'): - self.message_subscribe_users(user_ids=[vals['owner_user_id']]) + self.message_subscribe(partner_ids=self.env['res.users'].browse(vals['owner_user_id']).partner_id.ids) return super(MaintenanceEquipment, self).write(vals) @api.model @@ -377,8 +377,8 @@ class MaintenanceRequest(models.Model): def _add_followers(self): for request in self: - user_ids = (request.owner_user_id + request.technician_user_id).ids - request.message_subscribe_users(user_ids=user_ids) + partner_ids = (request.owner_user_id.partner_id + request.technician_user_id.partner_id).ids + request.message_subscribe(partner_ids=partner_ids) @api.model def _read_group_stage_ids(self, stages, domain, order): diff --git a/addons/project/models/project.py b/addons/project/models/project.py index e46bbd11a82..f0e6a3394ac 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -358,16 +358,14 @@ class Project(models.Model): return super(Project, self).get_access_action(access_uid) @api.multi - def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, force=True): + def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): """ Subscribe to all existing active tasks when subscribing to a project """ - res = super(Project, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids, force=force) - if not subtype_ids or any(subtype.parent_id.res_model == 'project.task' for subtype in self.env['mail.message.subtype'].browse(subtype_ids)): - for partner_id in partner_ids or []: - self.mapped('tasks').filtered(lambda task: not task.stage_id.fold and partner_id not in task.message_partner_ids.ids).message_subscribe( - partner_ids=[partner_id], channel_ids=None, subtype_ids=None, force=False) - for channel_id in channel_ids or []: - self.mapped('tasks').filtered(lambda task: not task.stage_id.fold and channel_id not in task.message_channel_ids.ids).message_subscribe( - partner_ids=None, channel_ids=[channel_id], subtype_ids=None, force=False) + res = super(Project, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids) + project_subtypes = self.env['mail.message.subtype'].browse(subtype_ids) if subtype_ids else None + task_subtypes = project_subtypes.mapped('parent_id').ids if project_subtypes else None + if not subtype_ids or task_subtypes: + self.mapped('tasks').message_subscribe( + partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=task_subtypes) return res @api.multi diff --git a/addons/project/tests/test_access_rights.py b/addons/project/tests/test_access_rights.py index 0016a71f71f..fe184147492 100644 --- a/addons/project/tests/test_access_rights.py +++ b/addons/project/tests/test_access_rights.py @@ -79,7 +79,7 @@ class TestPortalProject(TestPortalProjectBase): # Do: Donovan reads project -> ko (public ko employee) self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id']) - pigs.message_subscribe_users(user_ids=[self.user_projectuser.id]) + pigs.message_subscribe(partner_ids=[self.user_projectuser.partner_id.id]) # Do: Alfred reads project -> ok (follower ok followers) prout = pigs.sudo(self.user_projectuser) @@ -93,7 +93,7 @@ class TestPortalProject(TestPortalProjectBase): 'name': 'Pigs task', 'project_id': pigs.id }) # not follower user should not be able to create a task - pigs.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_projectuser.id]) + pigs.sudo(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_projectuser.partner_id.id]) self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_projectuser).with_context({ 'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id}) diff --git a/addons/project/tests/test_portal.py b/addons/project/tests/test_portal.py index 6413488da2e..ba18b5a5a16 100644 --- a/addons/project/tests/test_portal.py +++ b/addons/project/tests/test_portal.py @@ -25,9 +25,9 @@ class TestPortalProject(TestPortalProjectBase): self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_noone).search, [('project_id', '=', pigs.id)]) # Data: task follower - pigs.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id]) - self.task_1.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id]) - self.task_3.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id]) + pigs.sudo(self.user_projectmanager).message_subscribe(partner_ids=[self.user_portal.partner_id.id]) + self.task_1.sudo(self.user_projectuser).message_subscribe(partner_ids=[self.user_portal.partner_id.id]) + self.task_3.sudo(self.user_projectuser).message_subscribe(partner_ids=[self.user_portal.partner_id.id]) # Do: Chell reads project -> ok (portal ok public) pigs.sudo(self.user_portal).read(['user_id']) # Do: Donovan reads project -> ko (public ko portal) @@ -35,5 +35,5 @@ class TestPortalProject(TestPortalProjectBase): # Test: no access right to project.task self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_public).search, []) # Data: task follower cleaning - self.task_1.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id]) - self.task_3.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id]) + self.task_1.sudo(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_portal.partner_id.id]) + self.task_3.sudo(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_portal.partner_id.id]) diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py index 3d2600facce..20e94c288fd 100644 --- a/addons/test_mail/tests/test_mail_followers.py +++ b/addons/test_mail/tests/test_mail_followers.py @@ -26,7 +26,7 @@ class BaseFollowersTest(common.BaseFunctionalTest): test_record = self.test_record.sudo(self.user_employee) followed_before = test_record.search([('message_is_follower', '=', True)]) self.assertFalse(test_record.message_is_follower) - test_record.message_subscribe_users(user_ids=[self.user_employee.id]) + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id]) followed_after = test_record.search([('message_is_follower', '=', True)]) self.assertTrue(test_record.message_is_follower) self.assertEqual(followed_before | test_record, followed_after) @@ -80,7 +80,7 @@ class BaseFollowersTest(common.BaseFunctionalTest): self.assertEqual(follower, test_record.message_follower_ids) self.assertEqual(follower.subtype_ids, self.mt_mg_nodef) - def test_followers_multiple_subscription(self): + def test_followers_multiple_subscription_force(self): test_record = self.test_record.sudo(self.user_employee) test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id]) @@ -102,7 +102,7 @@ class BaseFollowersTest(common.BaseFunctionalTest): self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef) # set new subtypes with force=False, meaning no rewriting of the subscription is done -> result should not change - test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], force=False) + test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id) self.assertEqual(test_record.message_channel_ids, self.env['mail.channel']) self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef) diff --git a/addons/test_mail/tests/test_message_compose.py b/addons/test_mail/tests/test_message_compose.py index 08b7760f3bd..517f0faeb1c 100644 --- a/addons/test_mail/tests/test_message_compose.py +++ b/addons/test_mail/tests/test_message_compose.py @@ -32,7 +32,7 @@ class TestMessagePost(BaseFunctionalTest, MockEmails, TestRecipients): _body, _body_alt, _subject = '

Test Body

', 'Test Body', 'Test Subject' # subscribe second employee to the group to test notifications - self.test_record.message_subscribe_users(user_ids=[self.user_admin.id]) + self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) msg = self.test_record.sudo(self.user_employee).message_post( body=_body, subject=_subject, @@ -61,7 +61,7 @@ class TestMessagePost(BaseFunctionalTest, MockEmails, TestRecipients): @mute_logger('odoo.addons.mail.models.mail_mail') def test_post_notifications_keep_emails(self): - self.test_record.message_subscribe_users(user_ids=[self.user_admin.id]) + self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) msg = self.test_record.sudo(self.user_employee).message_post( body='Test', subject='Test', diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index feca36fde86..8ac43339e4a 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -99,7 +99,7 @@ class TestMailPerformance(TransactionCase): @users('admin', 'emp') @warmup def test_create_mail_simple(self): - with self.assertQueryCount(admin=9, emp=9): # test_mail only: 9 - 9 + with self.assertQueryCount(admin=8, emp=8): # test_mail only: 8 - 8 self.env['mail.test.simple'].create({'name': 'Test'}) @users('admin', 'emp') @@ -160,7 +160,7 @@ class TestAdvMailPerformance(TransactionCase): 'default_res_model': 'mail.test.activity', }) - with self.assertQueryCount(admin=27, emp=33): # test_mail only: 26 - 32 + with self.assertQueryCount(admin=11, emp=15): # test_mail only: 11 - 15 activity = MailActivity.create({ 'summary': 'Test Activity', 'res_id': record.id, @@ -176,7 +176,7 @@ class TestAdvMailPerformance(TransactionCase): def test_adv_activity_mixin(self): record = self.env['mail.test.activity'].create({'name': 'Test'}) - with self.assertQueryCount(admin=27, emp=33): # test_mail only: 26 - 32 + with self.assertQueryCount(admin=11, emp=15): # test_mail only: 11 - 15 record.action_start('Test Start') record.write({'name': 'Dupe write'}) @@ -193,7 +193,7 @@ class TestAdvMailPerformance(TransactionCase): self.user_test.write({'notification_type': 'email'}) record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(admin=88, emp=115): # test_mail only: 86 - 113 + with self.assertQueryCount(admin=86, emp=111): # test_mail only: 84 - 109 record.write({ 'user_id': self.user_test.id, }) @@ -203,7 +203,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_assignation_inbox(self): record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(admin=48, emp=63): # test_mail only: 48 - 63 + with self.assertQueryCount(admin=47, emp=60): # com runbot 46 - 59 // test_mail only: 46 - 59 record.write({ 'user_id': self.user_test.id, }) @@ -223,7 +223,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_log_with_post(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(admin=27, emp=38): # test_mail only: 27 - 38 + with self.assertQueryCount(admin=24, emp=34): # test_mail only: 24 - 34 record.message_post( body='

Test message_post as log

', subtype='mail.mt_note', @@ -234,7 +234,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_no_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(admin=27, emp=38): # test_mail only: 27 - 38 + with self.assertQueryCount(admin=24, emp=34): # test_mail only: 24 - 34 record.message_post( body='

Test Post Performances basic

', partner_ids=[], @@ -247,7 +247,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(admin=78, emp=105): # com runbot 76 - 103 // test_mail only: 76 - 103 + with self.assertQueryCount(admin=75, emp=101): # com runbot 73 - 99 // test_mail only: 73 - 99 record.message_post( body='

Test Post Performances with an email ping

', partner_ids=self.customer.ids, @@ -259,7 +259,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_inbox_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(admin=40, emp=52): # com runbot 38 - 50 // test_mail only: 40 - 52 + with self.assertQueryCount(admin=37, emp=48): # com runbot 35 - 46 // test_mail only: 37 - 48 record.message_post( body='

Test Post Performances with an inbox ping

', partner_ids=self.user_test.partner_id.ids, @@ -272,10 +272,10 @@ class TestAdvMailPerformance(TransactionCase): def test_message_subscribe_default(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(admin=7, emp=8): # test_mail only: 7 - 8 + with self.assertQueryCount(admin=6, emp=6): # test_mail only: 6 - 6 record.message_subscribe(partner_ids=self.user_test.partner_id.ids) - with self.assertQueryCount(admin=19, emp=19): # test_mail only: 19 - 19 + with self.assertQueryCount(admin=3, emp=3): # test_mail only: 3 - 3 record.message_subscribe(partner_ids=self.user_test.partner_id.ids) @mute_logger('odoo.models.unlink') @@ -285,10 +285,10 @@ class TestAdvMailPerformance(TransactionCase): record = self.env['mail.test.simple'].create({'name': 'Test'}) subtype_ids = (self.env.ref('test_mail.st_mail_test_simple_external') | self.env.ref('mail.mt_comment')).ids - with self.assertQueryCount(admin=7, emp=8): # test_mail only: 7 - 8 + with self.assertQueryCount(admin=5, emp=5): # test_mail only: 5 - 5 record.message_subscribe(partner_ids=self.user_test.partner_id.ids, subtype_ids=subtype_ids) - with self.assertQueryCount(admin=19, emp=19): # test_mail only: 18 - 18 + with self.assertQueryCount(admin=14, emp=14): # test_mail only: 14 - 14 record.message_subscribe(partner_ids=self.user_test.partner_id.ids, subtype_ids=subtype_ids) @@ -424,35 +424,33 @@ class TestHeavyMailPerformance(TransactionCase): self.assertEqual(rec.message_channel_ids, self.env['mail.channel']) # subscribe new followers with forced given subtypes - with self.assertQueryCount(admin=20, emp=21): # test_mail only: 20 - 21 + with self.assertQueryCount(admin=16, emp=16): # test_mail only: 16 - 16 rec.message_subscribe( partner_ids=pids[:4], channel_ids=cids, - subtype_ids=subtype_ids, + subtype_ids=subtype_ids ) self.assertEqual(rec.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners[:4]) self.assertEqual(rec.message_channel_ids, self.channel) # subscribe existing and new followers with force=False, meaning only some new followers will be added - with self.assertQueryCount(admin=11, emp=12): # test_mail only: 11 - 12 + with self.assertQueryCount(admin=8, emp=8): # test_mail only: 8 - 8 rec.message_subscribe( partner_ids=pids[:6], channel_ids=cids, - subtype_ids=None, - force=False, + subtype_ids=None ) self.assertEqual(rec.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners[:6]) self.assertEqual(rec.message_channel_ids, self.channel) # subscribe existing and new followers with force=True, meaning all will have the same subtypes - with self.assertQueryCount(admin=48, emp=49): # test_mail only: 48 - 49 + with self.assertQueryCount(admin=42, emp=43): # test_mail only: 42 - 43 rec.message_subscribe( partner_ids=pids, channel_ids=cids, - subtype_ids=subtype_ids, - force=True, + subtype_ids=subtype_ids ) self.assertEqual(rec.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners) @@ -471,7 +469,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id) - with self.assertQueryCount(admin=90, emp=119): # test_mail only: 88 - 117 + with self.assertQueryCount(admin=88, emp=115): # test_mail only: 86 - 113 rec.write({'user_id': self.user_portal.id}) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) @@ -494,7 +492,7 @@ class TestHeavyMailPerformance(TransactionCase): customer_id = self.customer.id user_id = self.user_portal.id - with self.assertQueryCount(admin=318, emp=379): # com runbot: 318 - 378 // test_mail only: 311 - 372 + with self.assertQueryCount(admin=266, emp=317): #com runbot: 266 - 316 // test_mail only: 259 - 310 rec = self.env['mail.test.full'].create({ 'name': 'Test', 'umbrella_id': umbrella_id, @@ -523,7 +521,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(admin=227, emp=264): # test_mail only: 223 - 260 + with self.assertQueryCount(admin=178, emp=205): # test_mail only: 174 - 201 rec.write({ 'name': 'Test2', 'umbrella_id': self.umbrella.id, @@ -561,7 +559,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(admin=230, emp=268): # test_mail only: 225 - 264 + with self.assertQueryCount(admin=181, emp=210): # test_mail only: 176 - 205 rec.write({ 'name': 'Test2', 'umbrella_id': umbrella_id, @@ -595,7 +593,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) - with self.assertQueryCount(admin=63, emp=90): # test_mail only: 61 - 88 + with self.assertQueryCount(admin=61, emp=86): # test_mail only: 59 - 84 rec.write({ 'name': 'Test2', 'customer_id': customer_id, diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index eaf17861741..ec7d6b806df 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -135,7 +135,7 @@ class WebsiteEventTrackController(http.Controller): 'description': escape(post['description']) }) if request.env.user != request.website.user_id: - track.sudo().message_subscribe_users(user_ids=request.env.user.ids) + track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids) else: partner = request.env['res.partner'].sudo().search([('email', '=', post['email_from'])]) if partner: diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index d390570d448..410ef170ebb 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -392,7 +392,7 @@ class Slide(models.Model): if not self.user_has_groups('website.group_website_publisher'): values['website_published'] = False slide = super(Slide, self).create(values) - slide.channel_id.message_subscribe_users() + slide.channel_id.message_subscribe(partner_ids=self.env.user.partner_id.ids) slide._post_publication() return slide From 1eba78381d0b8053d6244dbc0025018aa66c7ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 3 Apr 2018 15:42:41 +0200 Subject: [PATCH 4/6] [REF] mail: improve implementation of auto subscription This commit refactors mail.thread message_auto_subscribe method. Purpose of this method is to automatically subscribe people to documents in several scenarios * followers from an umbrella record to sub-records, like followers of project added to tasks; * responsible of documents, in addition to sending notifications to newly added responsibles; * some specific use case depending on some applications like managers for expense reports; New auto subscription now uses the recently introduced method optimized to compute new or to-update followers. It is also rewritten so that overriding it is easier. Indeed simply overriding _message_auto_subscribe_followers is now sufficient to add new followers to a record, depending on some updated values. It is also now possible to specify a template to use for some specific notification, if requested. Fetching auto-subscription data in subtype is also optimized. It is now done in a single SQL query. All data related to parent (like project) and child (like task) as well as default and internal-only data is prepared in a single query and given back to auto subscription process. This allows to gain queries in auto subscription based on subtype like project / task use case. This commit has a great impact on performance counters. Complex use cases involving tracking and responsible notifications gain about 30 queries which is about 15% of their total query count. --- addons/mail/models/mail_message_subtype.py | 43 +++-- addons/mail/models/mail_thread.py | 196 +++++++++++---------- addons/test_mail/tests/test_performance.py | 16 +- 3 files changed, 140 insertions(+), 115 deletions(-) diff --git a/addons/mail/models/mail_message_subtype.py b/addons/mail/models/mail_message_subtype.py index f0bc054bd5e..0ebe7a86a11 100644 --- a/addons/mail/models/mail_message_subtype.py +++ b/addons/mail/models/mail_message_subtype.py @@ -56,17 +56,40 @@ class MailMessageSubtype(models.Model): self.clear_caches() return super(MailMessageSubtype, self).unlink() - @api.model - def auto_subscribe_subtypes(self, model_name): - """ Retrieve the header subtypes and relations for the given model. """ - subtype_ids, relations = self._auto_subscribe_subtypes(model_name) - return self.browse(subtype_ids), relations - @tools.ormcache('self.env.uid', 'model_name') - def _auto_subscribe_subtypes(self, model_name): - domain = ['|', ('res_model', '=', False), ('parent_id.res_model', '=', model_name)] - subtypes = self.search(domain) - return subtypes.ids, set(subtype.relation_field for subtype in subtypes if subtype.relation_field) + def _get_auto_subscription_subtypes(self, model_name): + """ Return data related to auto subscription based on subtype matching. + Example with tasks and project : + + * generic: discussion, res_model = False + * task: new, res_model = project.task + * project: task_new, parent_id = new, res_model = project.project, field = project_id + + Returned data + + * all_ids: all subtypes that are generic or related to task and project + * def_ids: for task, default subtypes ids + * int_ids: for task, internal-only default subtypes ids + * parent: dict(parent subtype id, child subtype id), i.e. {task_new.id: new.id} + * relation: dict(parent_model, relation_fields), i.e. {'project.project': ['project_id']} + """ + all_ids, def_ids, int_ids, parent, relation = list(), list(), list(), dict(), dict() + subtypes = self.sudo().search([ + '|', '|', ('res_model', '=', False), + ('res_model', '=', model_name), + ('parent_id.res_model', '=', model_name) + ]) + for subtype in subtypes: + if not subtype.res_model or subtype.res_model == model_name: + all_ids += subtype.ids + if subtype.default: + def_ids += subtype.ids + elif subtype.relation_field: + parent[subtype.id] = subtype.parent_id.id + relation.setdefault(subtype.res_model, set()).add(subtype.relation_field) + if subtype.internal: + int_ids += subtype.ids + return all_ids, def_ids, int_ids, parent, relation @api.model def default_subtypes(self, model_name): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 6891e016ced..82a917482e3 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -234,7 +234,7 @@ class MailThread(models.AbstractModel): for key, val in self._context.items(): if key.startswith('default_') and key[8:] not in create_values: create_values[key[8:]] = val - thread.message_auto_subscribe(list(create_values), values=create_values) + thread._message_auto_subscribe(create_values) # track values if not self._context.get('mail_notrack'): @@ -271,7 +271,7 @@ class MailThread(models.AbstractModel): result = super(MailThread, self).write(values) # update followers - self.message_auto_subscribe(list(values), values=values) + self._message_auto_subscribe(values) # Perform the tracking if tracked_fields: @@ -2108,52 +2108,64 @@ class MailThread(models.AbstractModel): ('channel_id', 'in', channel_ids or []) ]).unlink() - @api.model - def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None): - """ Returns the list of relational fields linking to res.users that should - trigger an auto subscribe. The default list checks for the fields - - called 'user_id' - - linking to res.users - - with track_visibility set - In OpenERP V7, this is sufficent for all major addon such as opportunity, - project, issue, recruitment, sale. - Override this method if a custom behavior is needed about fields - that automatically subscribe users. + def _message_auto_subscribe_followers(self, updated_values, default_subtype_ids): + """ Optional method to override in addons inheriting from mail.thread. + Return a list tuples containing ( + partner ID, + subtype IDs (or False if model-based default subtypes), + QWeb template XML ID for notification (or False is no specific + notification is required), + ), aka partners and their subtype and possible notification to send + using the auto subscription mechanism linked to updated values. + + Default value of this method is to return the new responsible of + documents. This is done using relational fields linking to res.users + with track_visibility set. Since OpenERP v7 it is considered as being + responsible for the document and therefore standard behavior is to + subscribe the user and send him a notification. + + Override this method to change that behavior and/or to add people to + notify, using possible custom notification. + + :param updated_values: see ``_message_auto_subscribe`` + :param default_subtype_ids: coming from ``_get_auto_subscription_subtypes`` """ - if auto_follow_fields is None: - auto_follow_fields = ['user_id'] - user_field_lst = [] + fnames = [] for name, field in self._fields.items(): - if name in auto_follow_fields and name in updated_fields and getattr(field, 'track_visibility', False) and field.comodel_name == 'res.users': - user_field_lst.append(name) - return user_field_lst + if name == 'user_id' and updated_values.get(name) and getattr(field, 'track_visibility', False): + if field.comodel_name == 'res.users': + fnames.append(name) + + new_subscriptions = [] + user_ids = [updated_values[fname] for fname in fnames if updated_values[fname]] + if user_ids: + new_pids = self.env['res.partner'].sudo().search([('user_ids', 'in', user_ids), ('active', '=', True)]).ids + for new_pid in new_pids: + new_subscriptions.append((new_pid, default_subtype_ids, 'mail.message_user_assigned' if new_pid != self.env.user.partner_id.id else False)) + + return new_subscriptions @api.multi - def _message_auto_subscribe_notify(self, partner_ids): - """ Notify newly subscribed followers of the last posted message. - :param partner_ids : the list of partner to add as needaction partner of the last message - (This excludes the current partner) + def _message_auto_subscribe_notify(self, partner_ids, template): + """ Notify new followers, using a template to render the content of the + notification message. Notifications pushed are done using the standard + notification mechanism in mail.thread. It is either inbox either email + depending on the partner state: no user (email, customer), share user + (email, customer) or classic user (notification_type) + + :param partner_ids: IDs of partner to notify; + :param template: XML ID of template used for the notification; """ - if not partner_ids: + if not self or self.env.context.get('mail_auto_subscribe_no_notify'): return - if self.env.context.get('mail_auto_subscribe_no_notify'): - return - - # send the email only to the current record and not all the ids matching active_domain ! - # by default, send_mail for mass_mail use the active_domain instead of active_ids. - if 'active_domain' in self.env.context: - ctx = dict(self.env.context) - ctx.pop('active_domain') - self = self.with_context(ctx) - - assignation_tpl = self.env.ref('mail.message_user_assigned') + view = self.env['ir.ui.view'].browse(self.env['ir.model.data'].xmlid_to_res_id(template)) for record in self: values = { 'object': record, } - assignation_msg = assignation_tpl.render(values, engine='ir.qweb', minimal_qcontext=True) + assignation_msg = view.render(values, engine='ir.qweb', minimal_qcontext=True) assignation_msg = self.env['mail.thread']._replace_local_links(assignation_msg) record.message_notify( subject='You have been assigned to %s' % record.display_name, @@ -2167,75 +2179,65 @@ class MailThread(models.AbstractModel): ) @api.multi - def message_auto_subscribe(self, updated_fields, values=None): - """ Handle auto subscription. Two methods for auto subscription exist: + def _message_auto_subscribe(self, updated_values): + """ Handle auto subscription. Auto subscription is done based on two + main mechanisms - - tracked res.users relational fields, such as user_id fields. Those fields - must be relation fields toward a res.users record, and must have the - track_visilibity attribute set. - - using subtypes parent relationship: check if the current model being - modified has an header record (such as a project for tasks) whose followers - can be added as followers of the current records. Example of structure - with project and task: + * using subtypes parent relationship. For example following a parent record + (i.e. project) with subtypes linked to child records (i.e. task). See + mail.message.subtype ``_get_auto_subscription_subtypes``; + * calling _message_auto_subscribe_notify that returns a list of partner + to subscribe, as well as data about the subtypes and notification + to send. Base behavior is to subscribe responsible and notify them; - - st_project_1.parent_id = st_task_1 - - st_project_1.res_model = 'project.project' - - st_project_1.relation_field = 'project_id' - - st_task_1.model = 'project.task' + Adding application-specific auto subscription should be done by overriding + ``_message_auto_subscribe_followers``. It should return structured data + for new partner to subscribe, with subtypes and eventual notification + to perform. See that method for more details. - :param list updated_fields: list of updated fields to track - :param dict values: updated values; if None, the first record will be browsed - to get the values. Added after releasing 7.0, therefore - not merged with updated_fields argumment. + :param updated_values: values modifying the record trigerring auto subscription """ - new_partners, new_channels = dict(), dict() - - # fetch auto_follow_fields: res.users relation fields whose changes are tracked for subscription - user_field_lst = self._message_get_auto_subscribe_fields(updated_fields) - - # fetch header subtypes - subtypes, relation_fields = self.env['mail.message.subtype'].auto_subscribe_subtypes(self._name) - - # if no change in tracked field or no change in tracked relational field: quit - if not any(relation in relation_fields for relation in updated_fields) and not user_field_lst: + if not self: return True - # find followers of headers, update structure for new followers - headers = set() - for subtype in subtypes: - if subtype.relation_field and values.get(subtype.relation_field): - headers.add((subtype.res_model, values.get(subtype.relation_field))) - if headers: - header_domain = ['|'] * (len(headers) - 1) - for header in headers: - header_domain += ['&', ('res_model', '=', header[0]), ('res_id', '=', header[1])] - for header_follower in self.env['mail.followers'].sudo().search(header_domain): - for subtype in header_follower.subtype_ids: - if subtype.parent_id and subtype.parent_id.res_model == self._name: - new_subtype = subtype.parent_id - elif subtype.res_model is False: - new_subtype = subtype - else: - continue - if header_follower.partner_id: - new_partners.setdefault(header_follower.partner_id.id, set()).add(new_subtype.id) - else: - new_channels.setdefault(header_follower.channel_id.id, set()).add(new_subtype.id) + new_partners, new_channels = dict(), dict() - # add followers coming from res.users relational fields that are tracked - to_add_users = self.env['res.users'].sudo().browse([values[name] for name in user_field_lst if values.get(name)]).filtered(lambda u: u.partner_id.active) - for partner in to_add_users.mapped('partner_id'): - new_partners.setdefault(partner.id, None) + # fetch auto subscription subtypes data + updated_relation = dict() + all_ids, def_ids, int_ids, parent, relation = self.env['mail.message.subtype']._get_auto_subscription_subtypes(self._name) - for pid, subtypes in new_partners.items(): - subtypes = list(subtypes) if subtypes is not None else None - self.message_subscribe(partner_ids=[pid], subtype_ids=subtypes) - for cid, subtypes in new_channels.items(): - subtypes = list(subtypes) if subtypes is not None else None - self.message_subscribe(channel_ids=[cid], subtype_ids=subtypes) + # check effectively modified relation field + for res_model, fnames in relation.items(): + for field in (fname for fname in fnames if updated_values.get(fname)): + updated_relation.setdefault(res_model, set()).add(field) + udpated_fields = [fname for fnames in updated_relation.values() for fname in fnames if updated_values.get(fname)] - # remove the current user from the needaction partner to avoid to notify the author of the message - user_pids = [user.partner_id.id for user in to_add_users if user != self.env.user] - self._message_auto_subscribe_notify(user_pids) + if udpated_fields: + doc_data = [(model, [updated_values[fname] for fname in fnames]) for model, fnames in updated_relation.items()] + res = self.env['mail.followers']._get_subscription_data(doc_data, None, None, include_pshare=True) + for fid, rid, pid, cid, subtype_ids, pshare in res: + sids = [parent[sid] for sid in subtype_ids if parent.get(sid)] + sids += [sid for sid in subtype_ids if sid not in parent and sid in def_ids] + if pid: + new_partners[pid] = (set(sids) & set(all_ids)) - set(int_ids) if pshare else set(sids) & set(all_ids) + if cid: + new_channels[cid] = (set(sids) & set(all_ids)) - set(int_ids) + + notify_data = dict() + res = self._message_auto_subscribe_followers(updated_values, def_ids) + for pid, sids, template in res: + new_partners.setdefault(pid, sids) + if template: + notify_data.setdefault(template, list()).append(pid) + + self.env['mail.followers']._insert_followers( + self._name, self.ids, + list(new_partners), new_partners, + list(new_channels), new_channels, + check_existing=True, existing_policy='skip') + + # notify people from auto subscription, for example like assignation + for template, pids in notify_data.items(): + self._message_auto_subscribe_notify(pids, template) return True diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 8ac43339e4a..f399f812041 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -193,7 +193,7 @@ class TestAdvMailPerformance(TransactionCase): self.user_test.write({'notification_type': 'email'}) record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(admin=86, emp=111): # test_mail only: 84 - 109 + with self.assertQueryCount(admin=82, emp=107): # test_mail only: 80 - 105 record.write({ 'user_id': self.user_test.id, }) @@ -203,7 +203,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_assignation_inbox(self): record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(admin=47, emp=60): # com runbot 46 - 59 // test_mail only: 46 - 59 + with self.assertQueryCount(admin=42, emp=55): # test_mail only: 42 - 55 record.write({ 'user_id': self.user_test.id, }) @@ -247,7 +247,7 @@ class TestAdvMailPerformance(TransactionCase): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(admin=75, emp=101): # com runbot 73 - 99 // test_mail only: 73 - 99 + with self.assertQueryCount(admin=75, emp=101): # test_mail only: 73 - 99 record.message_post( body='

Test Post Performances with an email ping

', partner_ids=self.customer.ids, @@ -469,7 +469,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id) - with self.assertQueryCount(admin=88, emp=115): # test_mail only: 86 - 113 + with self.assertQueryCount(admin=84, emp=111): # test_mail only: 82 - 109 rec.write({'user_id': self.user_portal.id}) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) @@ -492,7 +492,7 @@ class TestHeavyMailPerformance(TransactionCase): customer_id = self.customer.id user_id = self.user_portal.id - with self.assertQueryCount(admin=266, emp=317): #com runbot: 266 - 316 // test_mail only: 259 - 310 + with self.assertQueryCount(admin=237, emp=286): # test_mail only: 230 - 279 rec = self.env['mail.test.full'].create({ 'name': 'Test', 'umbrella_id': umbrella_id, @@ -521,7 +521,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(admin=178, emp=205): # test_mail only: 174 - 201 + with self.assertQueryCount(admin=149, emp=173): # test_mail only: 144 - 168 rec.write({ 'name': 'Test2', 'umbrella_id': self.umbrella.id, @@ -559,7 +559,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(admin=181, emp=210): # test_mail only: 176 - 205 + with self.assertQueryCount(admin=155, emp=183): # test_mail only: 150 - 178 rec.write({ 'name': 'Test2', 'umbrella_id': umbrella_id, @@ -593,7 +593,7 @@ class TestHeavyMailPerformance(TransactionCase): }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) - with self.assertQueryCount(admin=61, emp=86): # test_mail only: 59 - 84 + with self.assertQueryCount(admin=61, emp=84): # test_mail only: 59 - 82 rec.write({ 'name': 'Test2', 'customer_id': customer_id, From ee64e8104c63e4dd408d7ffaa5e51ad29b330044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 2 Mar 2018 16:05:57 +0100 Subject: [PATCH 5/6] [REF] hr_expense: use new implementation for auto subscription Expense report has custom code to add people as followers. This code has several issues, notably in some cases it could try to add several times same people, leading to crash and issues. As auto subscription has been cleaned let us clean the code in hr expense. Using _message_auto_subscribe_followers it is now simpler to automatically add follower to expense reports. It is not necessary to add hackish custom code in create and write. One change is done in this commit. Some people are set as follower after record creation and not before. However this was necessary when tracking messages were not correctly triggered or auto subscription done after tracking. As process is now 1/ create 2/ auto subscribe 3/ tracking, messages linked to tracking will be correctly push to recipients automatically added as followers. --- addons/hr_expense/models/hr_expense.py | 51 ++++---------------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 5519ca8a888..cc827d6fce7 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -527,18 +527,10 @@ class HrExpenseSheet(models.Model): @api.model def create(self, vals): - self._create_set_followers(vals) sheet = super(HrExpenseSheet, self).create(vals) sheet.activity_update() return sheet - @api.multi - def write(self, vals): - res = super(HrExpenseSheet, self).write(vals) - if vals.get('employee_id'): - self._add_followers() - return res - @api.multi def unlink(self): for expense in self: @@ -561,41 +553,14 @@ class HrExpenseSheet(models.Model): return 'hr_expense.mt_expense_paid' return super(HrExpenseSheet, self)._track_subtype(init_values) - def _get_users_to_subscribe(self, employee=False): - users = self.env['res.users'] - employee = employee or self.employee_id - if employee.user_id: - users |= employee.user_id - if employee.parent_id: - users |= employee.parent_id.user_id - if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id: - users |= employee.department_id.manager_id.user_id - return users - - def _add_followers(self): - users = self._get_users_to_subscribe() - self.message_subscribe(partner_ids=users.mapped('partner_id').ids) - - @api.model - def _create_set_followers(self, values): - # Add the followers at creation, so they can be notified - employee_id = values.get('employee_id') - if not employee_id: - return - - employee = self.env['hr.employee'].browse(employee_id) - users = self._get_users_to_subscribe(employee=employee) - self.env.user - values['message_follower_ids'] = [] - MailFollowers = self.env['mail.followers'] - for partner in users.mapped('partner_id'): - values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {partner.id: None}, {})[0] - - if values.get('user_id') and values.get('user_id') != employee.user_id.id and values.get('user_id') != self.env.uid: - resp_partner = self.env['res.users'].browse(values['user_id']) - # Parsing ORM command to avoid adding several times the same partner - # TDE note: a better implementation should come in saas 11.3 or 11.4, allowing to normally remove that code - if resp_partner.partner_id.id not in (x[2].get('partner_id') for x in values['message_follower_ids'] if len(x) == 3): - values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {resp_partner.partner_id.id: None}, {})[0] + def _message_auto_subscribe_followers(self, updated_values, subtype_ids): + res = super(HrExpenseSheet, self)._message_auto_subscribe_followers(updated_values, subtype_ids) + if updated_values.get('employee_id'): + employee = self.env['hr.employee'].browse(updated_values['employee_id']) + users = employee.user_id | employee.parent_id.user_id | employee.department_id.manager_id.user_id + for pid in users.mapped('partner_id').ids: + res.append((pid, subtype_ids, False)) + return res # -------------------------------------------- # Actions From 2dbcbdd3f23f9d2f725e0dc410fc1e30956a9ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 9 Mar 2018 13:50:00 +0100 Subject: [PATCH 6/6] [REM] mail: remove now unused add_followers_command All code using it has been gradually replaced. It can be safely removed. --- addons/mail/models/mail_followers.py | 77 ---------------------------- 1 file changed, 77 deletions(-) diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index 270f2404251..fc07e4e3ae7 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -35,83 +35,6 @@ class Followers(models.Model): 'mail.message.subtype', string='Subtype', help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall.") - @api.model - def _add_follower_command(self, res_model, res_ids, partner_data, channel_data, force=True): - """ Please upate me - :param force: if True, delete existing followers before creating new one - using the subtypes given in the parameters - """ - force_mode = force or (all(partner_data.values()) and all(channel_data.values())) - generic = [] - specific = {} - existing = {} # {res_id: follower_ids} - p_exist = {} # {partner_id: res_ids} - c_exist = {} # {channel_id: res_ids} - - followers = self.sudo().search([ - '&', - '&', ('res_model', '=', res_model), ('res_id', 'in', res_ids), - '|', ('partner_id', 'in', list(partner_data)), ('channel_id', 'in', list(channel_data))]) - - if force_mode: - followers.unlink() - else: - for follower in followers: - existing.setdefault(follower.res_id, list()).append(follower) - if follower.partner_id: - p_exist.setdefault(follower.partner_id.id, list()).append(follower.res_id) - if follower.channel_id: - c_exist.setdefault(follower.channel_id.id, list()).append(follower.res_id) - - default_subtypes, _internal_subtypes, external_subtypes = \ - self.env['mail.message.subtype'].default_subtypes(res_model) - - if force_mode: - employee_pids = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_data)), ('share', '=', False)]).mapped('partner_id').ids - for pid, data in partner_data.items(): - if not data: - if pid not in employee_pids: - partner_data[pid] = external_subtypes.ids - else: - partner_data[pid] = default_subtypes.ids - for cid, data in channel_data.items(): - if not data: - channel_data[cid] = default_subtypes.ids - - # create new followers, batch ok - gen_new_pids = [pid for pid in partner_data if pid not in p_exist] - gen_new_cids = [cid for cid in channel_data if cid not in c_exist] - for pid in gen_new_pids: - generic.append([0, 0, {'res_model': res_model, 'partner_id': pid, 'subtype_ids': [(6, 0, partner_data.get(pid) or default_subtypes.ids)]}]) - for cid in gen_new_cids: - generic.append([0, 0, {'res_model': res_model, 'channel_id': cid, 'subtype_ids': [(6, 0, channel_data.get(cid) or default_subtypes.ids)]}]) - - # create new followers, each document at a time because of existing followers to avoid erasing - if not force_mode: - for res_id in res_ids: - command = [] - doc_followers = existing.get(res_id, list()) - - new_pids = set(partner_data) - set([sub.partner_id.id for sub in doc_followers if sub.partner_id]) - set(gen_new_pids) - new_cids = set(channel_data) - set([sub.channel_id.id for sub in doc_followers if sub.channel_id]) - set(gen_new_cids) - - # subscribe new followers - for new_pid in new_pids: - command.append((0, 0, { - 'res_model': res_model, - 'partner_id': new_pid, - 'subtype_ids': [(6, 0, partner_data.get(new_pid) or default_subtypes.ids)], - })) - for new_cid in new_cids: - command.append((0, 0, { - 'res_model': res_model, - 'channel_id': new_cid, - 'subtype_ids': [(6, 0, channel_data.get(new_cid) or default_subtypes.ids)], - })) - if command: - specific[res_id] = command - return generic, specific - # # Modifying followers change access rights to individual documents. As the # cache may contain accessible/inaccessible data, one has to refresh it.