diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index 4e4fa8e20a1..e58daf247d6 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -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( diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 0b7970610f7..1c926e7051e 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -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) ] @@ -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) ] diff --git a/addons/project/models/project.py b/addons/project/models/project.py index a449a16d1d4..1a981c5a759 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -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( diff --git a/addons/project/security/project_security.xml b/addons/project/security/project_security.xml index eecc68d1ccb..96f0f8f6975 100644 --- a/addons/project/security/project_security.xml +++ b/addons/project/security/project_security.xml @@ -49,14 +49,13 @@ - Project: employees: following required for follower-only projects + Project: Only invited users - ['|', - ('privacy_visibility', '!=', 'followers'), - '|', - ('message_partner_ids', 'in', [user.partner_id.id]), - ('message_channel_ids', 'in', user.partner_id.channel_ids.ids), - ] + [ + '|', + ('privacy_visibility', '!=', 'followers'), + ('allowed_internal_user_ids', 'in', user.ids), + ] @@ -73,12 +72,7 @@ [ '|', ('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), ] @@ -102,9 +96,8 @@ Project: portal users: portal and following [ - '&', - ('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), ] @@ -113,13 +106,8 @@ Project/Task: portal users: (portal and following project) or (portal and following task) [ - '|', - '&', - ('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), ] diff --git a/addons/project/tests/__init__.py b/addons/project/tests/__init__.py index a65ef6d88ce..4b37120321d 100644 --- a/addons/project/tests/__init__.py +++ b/addons/project/tests/__init__.py @@ -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 diff --git a/addons/project/tests/test_access_rights.py b/addons/project/tests/test_access_rights.py index a6e0ac0494f..2af3a2a2647 100644 --- a/addons/project/tests/test_access_rights.py +++ b/addons/project/tests/test_access_rights.py @@ -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!" diff --git a/addons/project/tests/test_portal.py b/addons/project/tests/test_portal.py deleted file mode 100644 index 5ae9bda4bb3..00000000000 --- a/addons/project/tests/test_portal.py +++ /dev/null @@ -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]) diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index d5b3446db05..2f8a8b502d4 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -194,7 +194,7 @@ -