[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:
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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">[
|
||||
'&',
|
||||
('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">[
|
||||
'|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('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,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
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}"/>
|
||||
|
||||
Reference in New Issue
Block a user