[IMP] project: Following a private project no longer required

=======
Purpose
=======

Currently for a user to be allowed to see a private project they must follow the
project. This means that they will be added as a follower of all new tasks in the
project which causes them to receive 1000000 notifications, especially if there are a
lot of tasks in a project.

==============
Specifications
==============

1) Add an Employee m2m field on project next to 'Invited Employees'
    - only visible if the 'Invited Employees' option is selected
    - the project should only be visible to employees selected there (regardless if they
      are followers or not)
2) Add an Employee m2m field on tasks below the 'Email cc' one in debug mode
    - only visible if the 'Invited Employees' option is selected on the related project
    - should be pre-filled with what is set on the project
    - the task should be visible to employees selected there (regardless if they are
      followers or not)
3) Add a Portal users m2m field on project next to 'Portal users and all employees'
    - only visible if the 'Portal users and all employees' option is selected
    - the project should only be visible to portal users selected there (regardless if
      they are followers or not)
    - rename the option into 'Invited portal users and all employees'
4) Add a Portal users m2m field on tasks below the 'Email cc' one in debug mode
    - only visible if the 'Portal users and all employees' option is selected
    - should be pre-filled with what is set on the project
    - the task should be visible to portal users selected there (regardless if they are
      followers or not)
5) Following a project/task should not grant the user/employee the ability to see the
   project/task

Task 2031527

closes odoo/odoo#40505

Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Lucas Lefèvre
2020-01-14 16:26:32 +00:00
parent 1227e40776
commit 7059b177ee
9 changed files with 286 additions and 156 deletions
+2 -2
View File
@@ -25,7 +25,7 @@ class AccountAnalyticLine(models.Model):
domain = [('allow_timesheets', '=', True)]
if not self.user_has_groups('hr_timesheet.group_timesheet_manager'):
return expression.AND([domain,
['|', ('privacy_visibility', '!=', 'followers'), ('message_partner_ids', 'in', [self.env.user.partner_id.id])]
['|', ('privacy_visibility', '!=', 'followers'), ('allowed_internal_user_ids', 'in', self.env.user.ids)]
])
return domain
@@ -36,7 +36,7 @@ class AccountAnalyticLine(models.Model):
def _domain_task_id(self):
if not self.user_has_groups('hr_timesheet.group_hr_timesheet_approver'):
return ['|', ('privacy_visibility', '!=', 'followers'), ('message_partner_ids', 'in', [self.env.user.partner_id.id])]
return ['|', ('privacy_visibility', '!=', 'followers'), ('allowed_user_ids', 'in', self.env.user.ids)]
return []
task_id = fields.Many2one(
@@ -38,8 +38,8 @@
('project_id', '!=', False),
'|', '|',
('project_id.privacy_visibility', '!=', 'followers'),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('task_id.message_partner_ids', 'in', [user.partner_id.id])
('project_id.allowed_internal_user_ids', 'in', user.ids),
('task_id.allowed_user_ids', 'in', user.ids)
]</field>
<field name="groups" eval="[(4, ref('group_hr_timesheet_user'))]"/>
</record>
@@ -51,7 +51,7 @@
('project_id', '!=', False),
'|',
('project_id.privacy_visibility', '!=', 'followers'),
('project_id.message_partner_ids', 'in', [user.partner_id.id])
('project_id.allowed_internal_user_ids', 'in', user.ids)
]</field>
<field name="groups" eval="[(4, ref('hr_timesheet.group_hr_timesheet_approver'))]" />
</record>
+60 -7
View File
@@ -190,9 +190,9 @@ class Project(models.Model):
help="Internal email associated with this project. Incoming emails are automatically synchronized "
"with Tasks (or optionally Issues if the Issue Tracker module is installed).")
privacy_visibility = fields.Selection([
('followers', 'Invited employees'),
('employees', 'All employees'),
('portal', 'Portal users and all employees'),
('followers', 'Invited internal users'),
('employees', 'All internal users'),
('portal', 'Invited portal users and all internal users'),
],
string='Visibility', required=True,
default='portal',
@@ -202,6 +202,11 @@ class Project(models.Model):
"- Portal users and all employees: employees may see everything."
" Portal users may see project and tasks followed by.\n"
" them or by someone of their company.")
allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users', inverse='_inverse_allowed_user')
allowed_internal_user_ids = fields.Many2many('res.users', 'project_allowed_internal_users_rel',
string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)])
allowed_portal_user_ids = fields.Many2many('res.users', 'project_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)])
doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached")
date_start = fields.Date(string='Start Date')
date = fields.Date(string='Expiration Date', index=True, tracking=True)
@@ -225,6 +230,18 @@ class Project(models.Model):
('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.')
]
@api.depends('allowed_internal_user_ids', 'allowed_portal_user_ids')
def _compute_allowed_users(self):
for project in self:
users = project.allowed_internal_user_ids | project.allowed_portal_user_ids
project.allowed_user_ids = users
def _inverse_allowed_user(self):
for project in self:
allowed_users = project.allowed_user_ids
project.allowed_portal_user_ids = allowed_users.filtered('share')
project.allowed_internal_user_ids = allowed_users - project.allowed_portal_user_ids
def _compute_access_url(self):
super(Project, self)._compute_access_url()
for project in self:
@@ -292,22 +309,33 @@ class Project(models.Model):
project = super(Project, self).create(vals)
if not vals.get('subtask_project_id'):
project.subtask_project_id = project.id
if project.privacy_visibility == 'portal' and project.partner_id:
project.message_subscribe(project.partner_id.ids)
if project.privacy_visibility == 'portal' and project.partner_id.user_ids:
project.allowed_user_ids |= project.partner_id.user_ids
return project
def write(self, vals):
allowed_users_changed = 'allowed_portal_user_ids' in vals or 'allowed_internal_user_ids' in vals
if allowed_users_changed:
allowed_users = {project: project.allowed_user_ids for project in self}
# directly compute is_favorite to dodge allow write access right
if 'is_favorite' in vals:
vals.pop('is_favorite')
self._fields['is_favorite'].determine_inverse(self)
res = super(Project, self).write(vals) if vals else True
if allowed_users_changed:
for project in self:
permission_removed = allowed_users.get(project) - project.allowed_user_ids
for task in project.task_ids:
task.allowed_user_ids -= permission_removed
if 'active' in vals:
# archiving/unarchiving a project does it on its tasks, too
self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']})
if vals.get('partner_id') or vals.get('privacy_visibility'):
for project in self.filtered(lambda project: project.privacy_visibility == 'portal'):
project.message_subscribe(project.partner_id.ids)
project.allowed_user_ids |= project.partner_id.user_ids
return res
def unlink(self):
@@ -498,7 +526,7 @@ class Task(models.Model):
partner_email = fields.Char(related='partner_id.email', string='Customer Email')
partner_phone = fields.Char(related='partner_id.phone')
partner_city = fields.Char(related='partner_id.city', readonly=False)
manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False)
manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True)
company_id = fields.Many2one('res.company', string='Company', required=True, default=_default_company_id)
color = fields.Integer(string='Color Index')
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False)
@@ -515,6 +543,8 @@ class Task(models.Model):
subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
email_from = fields.Char(string='Email', help="These people will receive email.", index=True,
compute='_compute_email_from', store="True", readonly=False)
allowed_user_ids = fields.Many2many('res.users', string="Visible to", groups='project.group_project_manager', compute='_compute_allowed_user_ids', store=True, readonly=False)
project_privacy_visibility = fields.Selection(related='project_id.privacy_visibility', string="Project Visibility")
# Computed field about working time elapsed between record creation and assignation/closing.
working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg")
working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg")
@@ -528,6 +558,14 @@ class Task(models.Model):
if not self._check_recursion():
raise ValidationError(_('Error! You cannot create recursive hierarchy of tasks.'))
@api.constrains('allowed_user_ids')
def _check_no_portal_allowed(self):
for task in self.filtered(lambda t: t.project_id.privacy_visibility != 'portal'):
portal_users = task.allowed_user_ids.filtered('share')
if portal_users:
user_names = ', '.join(portal_users[:10].mapped('name'))
raise ValidationError(_("The project visibility setting doesn't allow portal users to see the project's tasks. (%s)") % user_names)
@api.depends('date_deadline')
def _compute_date_deadline_formatted(self):
for task in self:
@@ -539,6 +577,21 @@ class Task(models.Model):
message_attachment_ids = task.mapped('message_ids.attachment_ids').ids # from mail_thread
task.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))]
@api.depends('project_id.allowed_user_ids', 'project_id.privacy_visibility')
def _compute_allowed_user_ids(self):
for task in self:
portal_users = task.allowed_user_ids.filtered('share')
internal_users = task.allowed_user_ids - portal_users
if task.project_id.privacy_visibility == 'followers':
task.allowed_user_ids |= task.project_id.allowed_internal_user_ids
task.allowed_user_ids -= portal_users
elif task.project_id.privacy_visibility == 'portal':
task.allowed_user_ids |= task.project_id.allowed_portal_user_ids
if task.project_id.privacy_visibility != 'portal':
task.allowed_user_ids -= portal_users
elif task.project_id.privacy_visibility != 'followers':
task.allowed_user_ids -= internal_users
@api.depends('create_date', 'date_end', 'date_assign')
def _compute_elapsed(self):
task_linked_to_calendar = self.filtered(
+11 -23
View File
@@ -49,14 +49,13 @@
</record>
<record model="ir.rule" id="project_public_members_rule">
<field name="name">Project: employees: following required for follower-only projects</field>
<field name="name">Project: Only invited users</field>
<field name="model_id" ref="model_project_project"/>
<field name="domain_force">['|',
('privacy_visibility', '!=', 'followers'),
'|',
('message_partner_ids', 'in', [user.partner_id.id]),
('message_channel_ids', 'in', user.partner_id.channel_ids.ids),
]</field>
<field name="domain_force">[
'|',
('privacy_visibility', '!=', 'followers'),
('allowed_internal_user_ids', 'in', user.ids),
]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
@@ -73,12 +72,7 @@
<field name="domain_force">[
'|',
('project_id.privacy_visibility', '!=', 'followers'),
'|',
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
'|',
('message_partner_ids', 'in', [user.partner_id.id]),
# to subscribe check access to the record, follower is not enough at creation
('user_id', '=', user.id)
('allowed_user_ids', 'in', user.ids),
]</field>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
@@ -102,9 +96,8 @@
<field name="name">Project: portal users: portal and following</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="domain_force">[
'&amp;',
('privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
('privacy_visibility', '=', 'portal'),
('allowed_portal_user_ids', 'in', user.ids),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
@@ -113,13 +106,8 @@
<field name="name">Project/Task: portal users: (portal and following project) or (portal and following task)</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
('project_id.privacy_visibility', '=', 'portal'),
('allowed_user_ids', 'in', user.ids),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# from . import test_project_base, test_project_flow, test_access_rights, test_project_ui, test_portal
from . import test_project_base, test_project_flow, test_access_rights, test_project_ui
from . import test_multicompany
+203 -80
View File
@@ -2,101 +2,224 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.project.tests.test_project_base import TestProjectCommon
from odoo.exceptions import AccessError
from odoo.tools import mute_logger
from odoo.exceptions import AccessError, ValidationError
from odoo.tests.common import new_test_user, users
class TestProjectPortalCommon(TestProjectCommon):
class TestAccessRights(TestProjectCommon):
def setUp(self):
super(TestProjectPortalCommon, self).setUp()
self.user_noone = self.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True}).create({
'name': 'Noemie NoOne',
'login': 'noemie',
'email': 'n.n@example.com',
'signature': '--\nNoemie',
'notification_type': 'email',
'groups_id': [(6, 0, [])]})
super().setUp()
self.task = self.create_task('Make the world a better place')
self.user = new_test_user(self.env, 'Internal user', groups='base.group_user')
self.portal = new_test_user(self.env, 'Portal user', groups='base.group_portal')
self.task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test3', 'user_id': self.user_portal.id, 'project_id': self.project_pigs.id})
self.task_4 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test4', 'user_id': self.user_public.id, 'project_id': self.project_pigs.id})
self.task_5 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test5', 'user_id': False, 'project_id': self.project_pigs.id})
self.task_6 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test5', 'user_id': False, 'project_id': self.project_pigs.id})
def create_task(self, name, *, with_user=None, **kwargs):
values = dict(name=name, project_id=self.project_pigs.id, **kwargs)
return self.env['project.task'].with_user(with_user or self.env.user).create(values)
class TestPortalProject(TestProjectPortalCommon):
class TestCRUDVisibilityFollowers(TestAccessRights):
@mute_logger('odoo.addons.base.models.ir_model')
def test_employee_project_access_rights(self):
pigs = self.project_pigs
def setUp(self):
super().setUp()
self.project_pigs.privacy_visibility = 'followers'
pigs.write({'privacy_visibility': 'employees'})
# Do: Alfred reads project -> ok (employee ok employee)
pigs.with_user(self.user_projectuser).read(['user_id'])
# Test: all project tasks visible
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('project_id', '=', pigs.id)])
test_task_ids = set([self.task_1.id, self.task_2.id, self.task_3.id, self.task_4.id, self.task_5.id, self.task_6.id])
self.assertEqual(set(tasks.ids), test_task_ids,
'access rights: project user cannot see all tasks of an employees project')
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.with_user(self.user_noone).read, ['user_id'])
# Do: Donovan reads project -> ko (public ko employee)
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
# Do: project user is employee and can create a task
tmp_task = self.env['project.task'].with_user(self.user_projectuser).with_context({'mail_create_nolog': True}).create({
'name': 'Pigs task',
'project_id': pigs.id})
tmp_task.with_user(self.user_projectuser).unlink()
@users('Internal user', 'Portal user')
def test_project_no_write(self):
with self.assertRaises(AccessError, msg="%s should not be able to write on the project" % self.env.user.name):
self.project_pigs.with_user(self.env.user).name = "Take over the world"
@mute_logger('odoo.addons.base.models.ir_model')
def test_favorite_project_access_rights(self):
pigs = self.project_pigs.with_user(self.user_projectuser)
self.project_pigs.allowed_user_ids = self.env.user
with self.assertRaises(AccessError, msg="%s should not be able to write on the project" % self.env.user.name):
self.project_pigs.with_user(self.env.user).name = "Take over the world"
# we can't write on project name
self.assertRaises(AccessError, pigs.write, {'name': 'False Pigs'})
# we can write on is_favorite
pigs.write({'is_favorite': True})
@users('Internal user', 'Portal user')
def test_project_no_unlink(self):
self.project_pigs.task_ids.unlink()
with self.assertRaises(AccessError, msg="%s should not be able to unlink the project" % self.env.user.name):
self.project_pigs.with_user(self.env.user).unlink()
@mute_logger('odoo.addons.base.ir.ir_model')
def test_followers_project_access_rights(self):
pigs = self.project_pigs
pigs.write({'privacy_visibility': 'followers'})
pigs.flush(['privacy_visibility'])
# Do: Alfred reads project -> ko (employee ko followers)
self.assertRaises(AccessError, pigs.with_user(self.user_projectuser).read, ['user_id'])
# Test: no project task visible
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('project_id', '=', pigs.id)])
self.assertEqual(tasks, self.task_1,
'access rights: employee user should not see tasks of a not-followed followers project, only assigned')
self.project_pigs.allowed_user_ids = self.env.user
self.project_pigs.task_ids.unlink()
with self.assertRaises(AccessError, msg="%s should not be able to unlink the project" % self.env.user.name):
self.project_pigs.with_user(self.env.user).unlink()
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.with_user(self.user_noone).read, ['user_id'])
@users('Internal user', 'Portal user')
def test_project_no_read(self):
self.project_pigs.invalidate_cache()
with self.assertRaises(AccessError, msg="%s should not be able to read the project" % self.env.user.name):
self.project_pigs.with_user(self.env.user).name
# Do: Donovan reads project -> ko (public ko employee)
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
@users('Portal user')
def test_project_allowed_portal_no_read(self):
self.project_pigs.allowed_user_ids = self.env.user
self.project_pigs.invalidate_cache()
with self.assertRaises(AccessError, msg="%s should not be able to read the project" % self.env.user.name):
self.project_pigs.with_user(self.env.user).name
pigs.message_subscribe(partner_ids=[self.user_projectuser.partner_id.id])
@users('Internal user')
def test_project_allowed_internal_read(self):
self.project_pigs.allowed_user_ids = self.env.user
self.project_pigs.invalidate_cache()
self.project_pigs.with_user(self.env.user).name
# Do: Alfred reads project -> ok (follower ok followers)
donkey = pigs.with_user(self.user_projectuser)
donkey.invalidate_cache()
donkey.read(['user_id'])
@users('Internal user', 'Portal user')
def test_task_no_read(self):
with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name):
self.task.with_user(self.env.user).name
# Do: Donovan reads project -> ko (public ko follower even if follower)
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
# Do: project user is follower of the project and can create a task
self.env['project.task'].with_user(self.user_projectuser).with_context({'mail_create_nolog': True}).create({
'name': 'Pigs task', 'project_id': pigs.id
})
# not follower user should not be able to create a task
pigs.with_user(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_projectuser.partner_id.id])
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_projectuser).with_context({
'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id})
@users('Portal user')
def test_task_allowed_portal_no_read(self):
self.project_pigs.allowed_user_ids = self.env.user
self.task.invalidate_cache()
with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name):
self.task.with_user(self.env.user).name
# Do: project user can create a task without project
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_projectuser).with_context({
'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id})
@users('Internal user')
def test_task_allowed_internal_read(self):
self.project_pigs.allowed_user_ids = self.env.user
self.task.invalidate_cache()
self.task.with_user(self.env.user).name
@users('Internal user', 'Portal user')
def test_task_no_write(self):
with self.assertRaises(AccessError, msg="%s should not be able to write on the task" % self.env.user.name):
self.task.with_user(self.env.user).name = "Paint the world in black & white"
self.project_pigs.allowed_user_ids = self.env.user
with self.assertRaises(AccessError, msg="%s should not be able to write on the task" % self.env.user.name):
self.task.with_user(self.env.user).name = "Paint the world in black & white"
@users('Internal user', 'Portal user')
def test_task_no_create(self):
with self.assertRaises(AccessError, msg="%s should not be able to create a task" % self.env.user.name):
self.create_task("Archive the world, it's not needed anymore")
self.project_pigs.allowed_user_ids = self.env.user
with self.assertRaises(AccessError, msg="%s should not be able to create a task" % self.env.user.name):
self.create_task("Archive the world, it's not needed anymore")
@users('Internal user', 'Portal user')
def test_task_no_unlink(self):
with self.assertRaises(AccessError, msg="%s should not be able to unlink the task" % self.env.user.name):
self.task.with_user(self.env.user).unlink()
self.project_pigs.allowed_user_ids = self.env.user
with self.assertRaises(AccessError, msg="%s should not be able to unlink the task" % self.env.user.name):
self.task.with_user(self.env.user).unlink()
class TestCRUDVisibilityPortal(TestAccessRights):
def setUp(self):
super().setUp()
self.project_pigs.privacy_visibility = 'portal'
@users('Portal user')
def test_task_portal_no_read(self):
self.task.invalidate_cache()
with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name):
self.task.with_user(self.env.user).name
@users('Portal user')
def test_task_allowed_portal_read(self):
self.project_pigs.allowed_user_ids = self.env.user
self.task.invalidate_cache()
self.task.with_user(self.env.user).name
@users('Internal user')
def test_task_internal_read(self):
self.task.with_user(self.env.user).name
class TestCRUDVisibilityEmployees(TestAccessRights):
def setUp(self):
super().setUp()
self.project_pigs.privacy_visibility = 'employees'
@users('Portal user')
def test_task_portal_no_read(self):
with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name):
self.task.with_user(self.env.user).name
self.project_pigs.allowed_user_ids = self.env.user
self.task.invalidate_cache()
with self.assertRaises(AccessError, msg="%s should not be able to read the task" % self.env.user.name):
self.task.with_user(self.env.user).name
@users('Internal user')
def test_task_allowed_portal_read(self):
self.task.invalidate_cache()
self.task.with_user(self.env.user).name
class TestAllowedUsers(TestAccessRights):
def setUp(self):
super().setUp()
self.project_pigs.privacy_visibility = 'followers'
def test_project_permission_added(self):
self.project_pigs.allowed_user_ids = self.user
self.assertIn(self.user, self.task.allowed_user_ids)
def test_project_default_permission(self):
self.project_pigs.allowed_user_ids = self.user
task = self.create_task("Review the end of the world")
self.assertIn(self.user, task.allowed_user_ids)
def test_project_default_customer_permission(self):
self.project_pigs.privacy_visibility = 'portal'
self.project_pigs.partner_id = self.portal.partner_id
self.assertIn(self.portal, self.task.allowed_user_ids)
self.assertIn(self.portal, self.project_pigs.allowed_user_ids)
def test_project_permission_removed(self):
self.project_pigs.allowed_user_ids = self.user
self.project_pigs.allowed_user_ids -= self.user
self.assertNotIn(self.user, self.task.allowed_user_ids)
def test_project_specific_permission(self):
self.project_pigs.allowed_user_ids = self.user
john = new_test_user(self.env, 'John')
self.task.allowed_user_ids |= john
self.project_pigs.allowed_user_ids -= self.user
self.assertIn(john, self.task.allowed_user_ids, "John should still be allowed to read the task")
def test_project_specific_remove_mutliple_tasks(self):
self.project_pigs.allowed_user_ids = self.user
john = new_test_user(self.env, 'John')
task = self.create_task('task')
self.task.allowed_user_ids |= john
self.project_pigs.allowed_user_ids -= self.user
self.assertIn(john, self.task.allowed_user_ids)
self.assertNotIn(john, task.allowed_user_ids)
self.assertNotIn(self.user, task.allowed_user_ids)
self.assertNotIn(self.user, self.task.allowed_user_ids)
def test_no_portal_allowed(self):
with self.assertRaises(ValidationError, msg="It should not allow to add portal users"):
self.task.allowed_user_ids = self.portal
def test_visibility_changed(self):
self.project_pigs.privacy_visibility = 'portal'
self.task.allowed_user_ids |= self.portal
self.assertNotIn(self.user, self.task.allowed_user_ids, "Internal user should have been removed from allowed users")
self.project_pigs.privacy_visibility = 'employees'
self.assertNotIn(self.portal, self.task.allowed_user_ids, "Portal user should have been removed from allowed users")
def test_write_task(self):
self.user.groups_id |= self.env.ref('project.group_project_user')
self.assertNotIn(self.user, self.project_pigs.allowed_user_ids)
self.task.allowed_user_ids = self.user
self.project_pigs.invalidate_cache()
self.task.invalidate_cache()
self.task.with_user(self.user).name = "I can edit a task!"
def test_no_write_project(self):
self.user.groups_id |= self.env.ref('project.group_project_user')
self.assertNotIn(self.user, self.project_pigs.allowed_user_ids)
with self.assertRaises(AccessError, msg="User should not be able to edit project"):
self.project_pigs.with_user(self.user).name = "I can't edit a task!"
-39
View File
@@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.project.tests.test_access_rights import TestProjectPortalCommon
from odoo.exceptions import AccessError
from odoo.tools import mute_logger
class TestPortalProject(TestProjectPortalCommon):
@mute_logger('odoo.addons.base.models.ir_model')
def test_portal_project_access_rights(self):
pigs = self.project_pigs
pigs.write({'privacy_visibility': 'portal'})
# Do: Alfred reads project -> ok (employee ok public)
pigs.with_user(self.user_projectuser).read(['user_id'])
# Test: all project tasks visible
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('project_id', '=', pigs.id)])
self.assertEqual(tasks, self.task_1 | self.task_2 | self.task_3 | self.task_4 | self.task_5 | self.task_6,
'access rights: project user should see all tasks of a portal project')
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.with_user(self.user_noone).read, ['user_id'])
# Test: no project task searchable
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_noone).search, [('project_id', '=', pigs.id)])
# Data: task follower
pigs.with_user(self.user_projectmanager).message_subscribe(partner_ids=[self.user_portal.partner_id.id])
self.task_1.with_user(self.user_projectuser).message_subscribe(partner_ids=[self.user_portal.partner_id.id])
self.task_3.with_user(self.user_projectuser).message_subscribe(partner_ids=[self.user_portal.partner_id.id])
# Do: Chell reads project -> ok (portal ok public)
pigs.with_user(self.user_portal).read(['user_id'])
# Do: Donovan reads project -> ko (public ko portal)
self.assertRaises(AccessError, pigs.with_user(self.user_public).read, ['user_id'])
# Test: no access right to project.task
self.assertRaises(AccessError, self.env['project.task'].with_user(self.user_public).search, [])
# Data: task follower cleaning
self.task_1.with_user(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_portal.partner_id.id])
self.task_3.with_user(self.user_projectuser).message_unsubscribe(partner_ids=[self.user_portal.partner_id.id])
@@ -194,7 +194,7 @@
</div>
</t>
<t t-set="card_body">
<div class="mb-1" t-if="user.partner_id.id in task.sudo().project_id.message_partner_ids.ids">
<div class="mb-1" t-if="user in task.sudo().project_id.allowed_user_ids">
<strong>Project:</strong> <a t-attf-href="/my/project/#{task.project_id.id}" t-field="task.project_id.name"/>
</div>
+5
View File
@@ -132,6 +132,8 @@
<group>
<field name="analytic_account_id" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]" context="{'default_partner_id': partner_id}" groups="analytic.group_analytic_accounting"/>
<field name="privacy_visibility" widget="radio"/>
<field name="allowed_internal_user_ids" widget="many2many_tags" attrs="{'invisible': [('privacy_visibility', '!=', 'followers')]}"/>
<field name="allowed_portal_user_ids" widget="many2many_tags" attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}"/>
<field name="subtask_project_id" groups="project.group_subtask_project"/>
<field name="company_id" groups="base.group_multi_company"/>
@@ -506,6 +508,9 @@
<field name="sequence" groups="base.group_no_one"/>
<field name="email_from" invisible="1"/>
<field name="email_cc" groups="base.group_no_one"/>
<field name="project_privacy_visibility" groups="base.group_no_one"/>
<field name="allowed_user_ids" widget="many2many_tags"
groups="base.group_no_one" attrs="{'invisible': [('project_privacy_visibility', 'not in', ('followers', 'portal'))]}"/>
<field name="child_ids" invisible="1" />
<field name="subtask_project_id" invisible="1" />
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>