diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 7ac69f4dc8f..5483d98af7d 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -218,6 +218,9 @@ class MailActivity(models.Model): valid = super(MailActivity, self)._filter_access_rules(operation) if valid and valid == self: return self + elif operation == 'read': + # Not in the ACL otherwise it would break the custom _search method + valid = self.sudo().filtered_domain([('user_id', '=', self.env.uid)]) else: valid = self.env[self._name] return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules') @@ -228,6 +231,8 @@ class MailActivity(models.Model): valid = super(MailActivity, self)._filter_access_rules_python(operation) if valid and valid == self: return self + elif operation == 'read': + valid = self.sudo().filtered_domain([('user_id', '=', self.env.uid)]) else: valid = self.env[self._name] return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules_python') @@ -413,14 +418,17 @@ class MailActivity(models.Model): f'"{self._table}"."id"', f'"{self._table}"."res_model"', f'"{self._table}"."res_id"', + f'"{self._table}"."user_id"', ) self.env.cr.execute(query_str, params) rows = self.env.cr.fetchall() # group res_ids by model, and determine accessible records + # Note: the user can read all activities assigned to him (see at the end of the method) model_ids = defaultdict(set) - for _id, res_model, res_id in rows: - model_ids[res_model].add(res_id) + for __, res_model, res_id, user_id in rows: + if user_id != self.env.uid: + model_ids[res_model].add(res_id) allowed_ids = defaultdict(set) for res_model, res_ids in model_ids.items(): @@ -433,8 +441,8 @@ class MailActivity(models.Model): activities = self.browse( id_ - for id_, res_model, res_id in rows - if res_id in allowed_ids[res_model] + for id_, res_model, res_id, user_id in rows + if user_id == self.env.uid or res_id in allowed_ids[res_model] ) return activities._as_query(order) diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 04a1cbb35ee..6c8df4d163a 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -278,14 +278,22 @@ class Users(models.Model): for activity in activities: record = self.env[activity.res_model].browse(activity.res_id) activities_by_record_by_model_name[activity.res_model][record] += activity - model_ids = list({self.env["ir.model"]._get(name).id for name in activities_by_record_by_model_name.keys()}) - user_activities = {} + activities_by_model_name = defaultdict(lambda: self.env["mail.activity"]) for model_name, activities_by_record in activities_by_record_by_model_name.items(): - domain = [("id", "in", list({r.id for r in activities_by_record.keys()}))] + if self.env[model_name].check_access_rights('read', raise_exception=False): + res_ids = [r.id for r in activities_by_record] + allowed_records = self.env[model_name].browse(res_ids)._filter_access_rules('read') + else: + allowed_records = self.env[model_name] + for record, activities in activities_by_record.items(): + if record not in allowed_records: + activities_by_model_name['mail.activity'] += activities + else: + activities_by_model_name[model_name] += activities + model_ids = [self.env["ir.model"]._get_id(name) for name in activities_by_model_name] + user_activities = {} + for model_name, activities in activities_by_model_name.items(): Model = self.env[model_name] - allowed_records = Model.search(domain) - if not allowed_records: - continue module = Model._original_module icon = module and modules.module.get_module_icon(module) model = self.env["ir.model"]._get(model_name).with_prefetch(model_ids) @@ -301,11 +309,12 @@ class Users(models.Model): "planned_count": 0, "view_type": getattr(Model, '_systray_view', 'list'), } - for record, activities in activities_by_record.items(): - if record not in allowed_records: - continue - for activity in activities: - user_activities[model_name]["%s_count" % activity.state] += 1 - if activity.state in ("today", "overdue"): - user_activities[model_name]["total_count"] += 1 + if model_name == 'mail.activity': + user_activities[model_name]['activity_ids'] = activities.ids + for activity in activities: + user_activities[model_name]["%s_count" % activity.state] += 1 + if activity.state in ("today", "overdue"): + user_activities[model_name]["total_count"] += 1 + if "mail.activity" in user_activities: + user_activities["mail.activity"]["name"] = _("Other activities") return list(user_activities.values()) diff --git a/addons/mail/static/src/core/web/activity_menu.js b/addons/mail/static/src/core/web/activity_menu.js index 72aa7b2e191..bd8e04eec49 100644 --- a/addons/mail/static/src/core/web/activity_menu.js +++ b/addons/mail/static/src/core/web/activity_menu.js @@ -31,8 +31,13 @@ export class ActivityMenu extends Component { this.sortActivityGroups(); } + /** + * Sort by model ID ASC but always place the activity group for "mail.activity" model at the end (other activities). + */ sortActivityGroups() { - this.store.activityGroups.sort((g1, g2) => g1.id - g2.id); + const getSortId = (activityGroup) => + activityGroup.model === "mail.activity" ? Number.MAX_VALUE : activityGroup.id; + this.store.activityGroups.sort((g1, g2) => getSortId(g1) - getSortId(g2)); } onBeforeOpen() { @@ -55,7 +60,12 @@ export class ActivityMenu extends Component { // So, duplicates are faking the count and "Load more" doesn't show up force_search_count: 1, }; - let domain = [["activity_user_id", "=", this.userId]]; + let domain; + if (group.model !== "mail.activity") { + domain = [["activity_user_id", "=", this.userId]]; + } else { + domain = [["id", "in", group.activity_ids]]; + } if (group.domain) { domain = Domain.and([domain, group.domain]).toList(); } diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py index f64a2ba6edd..aae2d186f37 100644 --- a/addons/test_mail/tests/test_mail_activity.py +++ b/addons/test_mail/tests/test_mail_activity.py @@ -141,6 +141,26 @@ class TestActivityRights(TestActivityCommon): 'test_mail.mail_act_test_todo', user_id=self.user_admin.id) + test_activity.user_id = self.user_employee + test_activity.flush_recordset() + + # user can read activities assigned to him even if he has no access to the document + with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash): + found = self.env['mail.activity'].with_user(self.user_employee).search( + [('id', '=', test_activity.id)]) + self.assertEqual(found, test_activity) + found.read(['summary']) + + # user can read_group activities assigned to him even if he has no access to the document + with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash): + read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group( + [('id', '=', test_activity.id)], + ['summary'], + ['summary'], + ) + self.assertEqual(1, read_group_result[0]['summary_count']) + self.assertEqual('Summary', read_group_result[0]['summary']) + @tests.tagged('mail_activity') class TestActivityFlow(TestActivityCommon):