[IMP] mail: display activities without record access in the systray
When scheduling plans on employees, activities may be assigned to people who don't have access to the record. It was managed locally in HR through a dummy model aggregating activities. However, it was removed and we replace it here by a generic solution that adds an entry in the systray: "Other activities". When clicking on that entry, it displays a list view of the activities assigned to the user for which he has no access to the related record. Those activities were not visible to the user before. Technical note: to allow displaying such records, we have modified the custom acl defined in python to allow users to read activities assigned to them even if they don't have access to the related record. Task-3562098 Part-of: odoo/odoo#139917
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user