[FIX] project: Fix user_ids access issues on project sharing

Both developments 6c4910a and 7563b44 have 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:
Yannick Tivisse (yti)
2021-10-18 14:01:36 +00:00
committed by Xavier BOL (xbo)
co-authored by Xavier BOL Raphael Collet
parent b2f170156a
commit 5652e7ee91
3 changed files with 106 additions and 20 deletions
+35
View File
@@ -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
+42 -9
View File
@@ -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.')
+29 -11
View File
@@ -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' : &quot;[('user_ids', 'in', [uid])]&quot;}" data-hotkey="q"/>
<button name="action_unassign_me" string="Unassign Me" type="object" class="oe_highlight"
attrs="{'invisible' : &quot;[('user_ids', 'not in', [uid])]&quot;}" 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', '&gt;', 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'}"/>