[FIX] project: Fix user_ids access issues on project sharing
Both developments6c4910aand7563b44have been introduced nearly at the same time in the saas-14.5 version, but the later one is introducing a side effect that breaks the project sharing feature. Indeed, before there was one assignee per task (user_id), and now we can assign several collaborators (user_ids). As the read on a M2O is just calling the name_get method, it wasn't an issue. Now, as it implies to read the res.users (and the res.partner) model, the assigned users were filtered according to a specific ir.rule for the portal users: <record id="res_partner_rule" model="ir.rule"> <field name="name">openerp.portal.res.partner</field> <field name="model_id" ref="base.model_res_partner"/> <field name="groups" eval="[(6,0,[ref('group_openerp_portal')])]"/> <field name="domain_force">[('id','child_of',user.commercial_partner_id.id)]</field> </record> This commit creates a new compute non-stored field called `portal_user_names` to display the name of all assignees in each task in the project sharing feature. Thus, the portal user can see all assignees via this char field. The `user_ids` field is removed in the views of project sharing since the portal cannot see all assignees with this field. By doing this, a collaborator cannot edit the field to assign or unassign himself to a task. To keep this behaviour, two buttons are added in the form view of task. One called 'Assign To Me', the current collaborator will be able to assign himself to the task when he will click on this button. The other button called 'Unassign Me' is to allow the collaborator to unassign himself to the task. part of task-2633229 closes odoo/odoo#78538 X-original-commit: 3b5a657df086b54def0bc0bdae89c19c89cb39f5 Signed-off-by: LTU-Odoo <IT-Ideas@users.noreply.github.com> Co-authored-by: Xavier BOL (xbo) <xbo@odoo.com> Co-authored-by: Raphael Collet <rco@odoo.com>
This commit is contained in:
committed by
Xavier BOL (xbo)
co-authored by
Xavier BOL
Raphael Collet
parent
b2f170156a
commit
5652e7ee91
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
<field name="color"/>
|
||||
<field name="priority"/>
|
||||
<field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/>
|
||||
<field name="user_ids"/>
|
||||
<!-- TODO: [XBO] remove me in master -->
|
||||
<field name="user_ids" invisible="1"/>
|
||||
<field name="portal_user_names"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="sequence"/>
|
||||
<field name="is_closed"/>
|
||||
@@ -89,8 +91,14 @@
|
||||
<field name="priority" widget="priority"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
|
||||
<span t-if="record.portal_user_names.raw_value.length > 0" class="pr-2" t-att-title="record.portal_user_names.raw_value">
|
||||
<t t-set="user_count" t-value="record.portal_user_names.raw_value.split(',').length"/>
|
||||
<t t-set="display_nb_assignees" t-value="user_count + ' assignee' + (user_count > 1 ? 's' : '')"/>
|
||||
<t t-out="display_nb_assignees"/>
|
||||
</span>
|
||||
<field name="kanban_state" widget="state_selection"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" options="{'no_open_chat': True}"/>
|
||||
<!-- TODO: [XBO] remove me in master -->
|
||||
<field name="user_ids" string="Assignees" widget="many2many_avatar_user" options="{'no_open_chat': True}" invisible="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +124,9 @@
|
||||
<field name="child_text" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="partner_id" optional="hide"/>
|
||||
<field name="user_ids" optional="show" widget="many2many_avatar_user" options="{'no_open_chat': True}"/>
|
||||
<field name="portal_user_names" string="Assignees" optional="show"/>
|
||||
<!-- TODO: [XBO] remove me in master -->
|
||||
<field name="user_ids" invisible="1" />
|
||||
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
|
||||
<field name="kanban_state" widget="state_selection" optional="hide"/>
|
||||
@@ -133,6 +143,10 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project Sharing: Task" class="o_form_project_tasks">
|
||||
<header>
|
||||
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight"
|
||||
attrs="{'invisible' : "[('user_ids', 'in', [uid])]"}" data-hotkey="q"/>
|
||||
<button name="action_unassign_me" string="Unassign Me" type="object" class="oe_highlight"
|
||||
attrs="{'invisible' : "[('user_ids', 'not in', [uid])]"}" data-hotkey="q"/>
|
||||
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}" />
|
||||
</header>
|
||||
<sheet string="Task">
|
||||
@@ -151,11 +165,10 @@
|
||||
<group>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="display_project_id" string="Project" invisible="1"/>
|
||||
<field name="user_ids"
|
||||
class="o_task_user_field"
|
||||
options="{'no_open': True, 'no_open_chat': True}"
|
||||
widget="many2many_avatar_user"
|
||||
domain="[('share', '=', False)]"/>
|
||||
<field name="user_ids" invisible="1" />
|
||||
<field name="portal_user_names"
|
||||
string="Assignees"
|
||||
class="o_task_user_field"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
@@ -182,7 +195,9 @@
|
||||
<field name="display_project_id" string="Project" optional="hide" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="partner_id" options="{'no_open': True, 'no_create': True, 'no_edit': True}" optional="hide"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" optional="show" options="{'no_open_chat': True}"/>
|
||||
<!-- TODO: [XBO] remove me in master -->
|
||||
<field name="user_ids" invisible="1"/>
|
||||
<field name="portal_user_names" string="Assignees"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
|
||||
<field name="kanban_state" widget="state_selection" optional="hide"/>
|
||||
@@ -208,7 +223,9 @@
|
||||
<search string="Tasks">
|
||||
<field name="name" string="Task"/>
|
||||
<field name="tag_ids"/>
|
||||
<field name="user_ids"/>
|
||||
<!-- TODO: [XBO] remove me in master -->
|
||||
<field name="user_ids" invisible="1"/>
|
||||
<field name="portal_user_names" string="Assignees"/>
|
||||
<field name="partner_id" operator="child_of"/>
|
||||
<field name="stage_id"/>
|
||||
<field string="Project" name="display_project_id"/>
|
||||
@@ -223,7 +240,8 @@
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
|
||||
<filter string="Assignees" name="user" context="{'group_by': 'user_ids'}"/>
|
||||
<!-- TODO: [XBO] remove me in master -->
|
||||
<filter string="Assignees" name="user" context="{'group_by': 'user_ids'}" invisible="1"/>
|
||||
<filter string="Project" name="project" context="{'group_by': 'project_id'}"/>
|
||||
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/>
|
||||
|
||||
Reference in New Issue
Block a user