diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 4baad1250ec..089d67ab8fc 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -38,6 +38,7 @@ PROJECT_TASK_READABLE_FIELDS = { 'displayed_image_id', 'display_name', 'priority', + 'portal_user_names', } PROJECT_TASK_WRITABLE_FIELDS = { @@ -956,6 +957,8 @@ class Task(models.Model): # Tracking of this field is done in the write function user_ids = fields.Many2many('res.users', relation='project_task_user_rel', column1='task_id', column2='user_id', string='Assignees', default=lambda self: self.env.user) + # User names displayed in project sharing views + portal_user_names = fields.Char(compute='_compute_portal_user_names', compute_sudo=True, search='_search_portal_user_names') # Second Many2many containing the actual personal stage for the current user # See project_task_stage_personal.py for the model defininition personal_stage_type_ids = fields.Many2many('project.task.type', 'project_task_user_rel', column1='task_id', column2='stage_id', @@ -1417,6 +1420,35 @@ class Task(models.Model): else: task.stage_id = False + @api.depends('user_ids') + def _compute_portal_user_names(self): + """ This compute method allows to see all the names of assigned users to each task contained in `self`. + + When we are in the project sharing feature, the `user_ids` contains only the users if we are a portal user. + That is, only the users in the same company of the current user. + So this compute method is a related of `user_ids.name` but with more records that the portal user + can normally see. + (In other words, this compute is only used in project sharing views to see all assignees for each task) + """ + if self.ids: + # fetch 'user_ids' in superuser mode (and override value in cache) + self._read(['user_ids']) + for task in self.with_context(prefetch_fields=False): + task.portal_user_names = ', '.join(task.user_ids.mapped('name')) + + def _search_portal_user_names(self, operator, value): + if operator != 'ilike' and not isinstance(value, str): + raise ValidationError('Not Implemented.') + + query = """ + SELECT task_user.task_id + FROM project_task_user_rel task_user + INNER JOIN res_users users ON task_user.user_id = users.id + INNER JOIN res_partner partners ON partners.id = users.partner_id + WHERE partners.name ILIKE %s + """ + return [('id', 'inselect', (query, [f'%{value}%']))] + @api.returns('self', lambda value: value.id) def copy(self, default=None): if default is None: @@ -1989,6 +2021,9 @@ class Task(models.Model): def action_assign_to_me(self): self.write({'user_ids': [(4, self.env.user.id)]}) + def action_unassign_me(self): + self.write({'user_ids': [Command.unlink(self.env.uid)]}) + # If depth == 1, return only direct children # If depth == 3, return children to third generation # If depth <= 0, return all children without depth limit diff --git a/addons/project/tests/test_project_sharing.py b/addons/project/tests/test_project_sharing.py index ff3ff4f948b..443df6df388 100644 --- a/addons/project/tests/test_project_sharing.py +++ b/addons/project/tests/test_project_sharing.py @@ -123,7 +123,7 @@ class TestProjectSharing(TestProjectSharingCommon): task = form.save() self.assertEqual(task.name, 'Test') self.assertEqual(task.project_id, self.project_portal) - self.assertEqual(task.user_ids, self.user_portal) + self.assertEqual(task.portal_user_names, self.user_portal.name) # 3.1) Try to change the project of the new task with this user. with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."): form.project_id = self.project_cows @@ -160,16 +160,49 @@ class TestProjectSharing(TestProjectSharingCommon): task = form.save() self.assertEqual(task.name, 'Test') self.assertEqual(task.project_id, self.project_cows) - # 3.1) Try to change the project of the new task with this user. - with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."): + + # 3.1) Try to change the project of the new task with this user. + with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."): + with self.get_project_sharing_form_view(task, self.user_portal) as form: form.project_id = self.project_portal - task = form.save() - # 3.2) Create a sub-task + + # 3.2) Create a sub-task + with self.get_project_sharing_form_view(task, self.user_portal) as form: with form.child_ids.new() as subtask_form: subtask_form.name = 'Test Subtask' with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."): subtask_form.display_project_id = self.project_portal - form.save() - self.assertEqual(task.child_ids.name, 'Test Subtask') - self.assertEqual(task.child_ids.project_id, self.project_cows) - self.assertEqual(task.child_ids.user_ids, self.user_portal) + self.assertEqual(task.child_ids.name, 'Test Subtask') + self.assertEqual(task.child_ids.project_id, self.project_cows) + self.assertEqual(task.child_ids.portal_user_names, self.user_portal.name) + self.assertEqual(task.child_ids.user_ids, self.user_portal) + + def test_portal_user_cannot_see_all_assignees(self): + """ Test when the portal sees a task he cannot see all the assignees. + + Because of a ir.rule in res.partner filters the assignees, the portal + can only see the assignees in the same company than him. + + Test Cases: + ========== + 1) add many assignees in a task + 2) check the portal user can read no assignee in this task. Should have an AccessError exception + """ + self.task_cow.write({'user_ids': [Command.link(self.user_projectmanager.id)]}) + with self.assertRaises(AccessError, msg="Should not accept the portal user to access to a task he does not follow it and its project."): + self.task_cow.with_user(self.user_portal).read(['portal_user_names']) + self.assertEqual(len(self.task_cow.user_ids), 2, '2 users should be assigned in this task.') + + project_share_wizard = self.env['project.share.wizard'].create({ + 'access_mode': 'edit', + 'res_model': 'project.project', + 'res_id': self.project_cows.id, + 'partner_ids': [ + Command.link(self.user_portal.partner_id.id), + ], + }) + project_share_wizard.action_send_mail() + + self.assertFalse(self.task_cow.with_user(self.user_portal).user_ids, 'the portal user should see no assigness in the task.') + task_portal_read = self.task_cow.with_user(self.user_portal).read(['portal_user_names']) + self.assertEqual(self.task_cow.portal_user_names, task_portal_read[0]['portal_user_names'], 'the portal user should see assignees name in the task via the `portal_user_names` field.') diff --git a/addons/project/views/project_sharing_views.xml b/addons/project/views/project_sharing_views.xml index 60cd3f88a5f..01264b38385 100644 --- a/addons/project/views/project_sharing_views.xml +++ b/addons/project/views/project_sharing_views.xml @@ -31,7 +31,9 @@ - + + + @@ -89,8 +91,14 @@
+ + + + + - + +
@@ -116,7 +124,9 @@ - + + + @@ -133,6 +143,10 @@
+
@@ -151,11 +165,10 @@ - + + @@ -182,7 +195,9 @@ - + + + @@ -208,7 +223,9 @@ - + + + @@ -223,7 +240,8 @@ domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/> - + +