From a28cc6333af3a1799fd5f5437a8f88de1b73b70f Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 23 Aug 2021 16:56:01 +0200 Subject: [PATCH] [REV] project: select access rights in project sharing This reverts commit fc7778f8c512202dc3361d6b87be8c28b299c3c8 because of a change in the spec. The access mode are removed and replaced by this access mode for portal user: - read: the user goes to the classic portal view - edit: the user is added as collaborator of the shared project and can access to project sharing views. To do this, the portal share is inherited by the project share wizard. This new wizard can be open to share in readonly and open to share in edit mode via 2 buttons in the form view of the shared project. A new stat button is added to form view of project to see the collaborators of this project. That is, the ones can access to the project sharing views. The project manager will can remove or also add new collaborators via the views in this stat button. task-2379518 closes #73341 --- .../security/hr_timesheet_security.xml | 2 +- addons/portal/wizard/portal_share.py | 90 +++++---- addons/project/__manifest__.py | 1 + addons/project/controllers/portal.py | 1 - .../controllers/project_sharing_chatter.py | 2 +- addons/project/models/__init__.py | 2 +- addons/project/models/project.py | 94 +++++----- addons/project/models/project_collaborator.py | 23 +++ .../project/models/project_sharing_access.py | 24 --- addons/project/models/res_partner.py | 17 ++ addons/project/security/ir.model.access.csv | 7 +- addons/project/security/project_security.xml | 12 +- .../src/project_sharing/project_sharing.js | 4 +- addons/project/tests/test_project_sharing.py | 172 +++++------------- .../views/project_collaborator_views.xml | 53 ++++++ .../project/views/project_sharing_views.xml | 7 +- addons/project/views/project_views.xml | 18 +- addons/project/wizard/project_share_wizard.py | 108 +++-------- .../wizard/project_share_wizard_views.xml | 39 ++-- 19 files changed, 314 insertions(+), 362 deletions(-) create mode 100644 addons/project/models/project_collaborator.py delete mode 100644 addons/project/models/project_sharing_access.py create mode 100644 addons/project/views/project_collaborator_views.xml diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 2b20fc60087..63d1b679d9d 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -39,7 +39,7 @@ ('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), ('task_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), ('project_id.privacy_visibility', '=', 'portal'), - ('project_id.project_sharing_access_ids.user_id', 'in', [user.id]), + ('project_id.collaborator_ids.partner_id', 'in', [user.partner_id.id]), ] diff --git a/addons/portal/wizard/portal_share.py b/addons/portal/wizard/portal_share.py index c8c36418a01..48f19e8ff22 100644 --- a/addons/portal/wizard/portal_share.py +++ b/addons/portal/wizard/portal_share.py @@ -17,13 +17,26 @@ class PortalShare(models.TransientModel): result['share_link'] = record.get_base_url() + record._get_share_url(redirect=True) return result + @api.model + def _selection_target_model(self): + return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([])] + res_model = fields.Char('Related Document Model', required=True) res_id = fields.Integer('Related Document ID', required=True) + resource_ref = fields.Reference('_selection_target_model', 'Related Document', compute='_compute_resource_ref') partner_ids = fields.Many2many('res.partner', string="Recipients", required=True) note = fields.Text(help="Add extra content to display in the email") share_link = fields.Char(string="Link", compute='_compute_share_link') access_warning = fields.Text("Access warning", compute="_compute_access_warning") + @api.depends('res_model', 'res_id') + def _compute_resource_ref(self): + for wizard in self: + if wizard.res_model and wizard.res_model in self.env: + wizard.resource_ref = '%s,%s' % (wizard.res_model, wizard.res_id or 0) + else: + wizard.resource_ref = None + @api.depends('res_model', 'res_id') def _compute_share_link(self): for rec in self: @@ -44,48 +57,61 @@ class PortalShare(models.TransientModel): record = res_model.browse(rec.res_id) rec.access_warning = record.access_warning + @api.model + def _get_note(self): + return self.env.ref('mail.mt_note') + + def _send_public_link(self, note, partners=None): + if not partners: + partners = self.partner_ids + for partner in partners: + share_link = self.resource_ref.get_base_url() + self.resource_ref._get_share_url(redirect=True, pid=partner.id) + saved_lang = self.env.lang + self = self.with_context(lang=partner.lang) + template = self.env.ref('portal.portal_share_template', False) + self.resource_ref.message_post_with_view(template, + values={'partner': partner, 'note': self.note, 'record': self.resource_ref, + 'share_link': share_link}, + subject=_("You are invited to access %s", self.resource_ref.display_name), + subtype_id=note.id, + email_layout_xmlid='mail.mail_notification_light', + partner_ids=[(6, 0, partner.ids)]) + self = self.with_context(lang=saved_lang) + + def _send_signup_link(self, note, partners=None): + if not partners: + partners = self.partner_ids.filtered(lambda partner: not partner.user_ids) + for partner in partners: + # prepare partner for signup and send singup url with redirect url + partner.signup_get_auth_param() + share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.res_model)[partner.id] + saved_lang = self.env.lang + self = self.with_context(lang=partner.lang) + template = self.env.ref('portal.portal_share_template', False) + self.resource_ref.message_post_with_view(template, + values={'partner': partner, 'note': self.note, 'record': self.resource_ref, + 'share_link': share_link}, + subject=_("You are invited to access %s", self.resource_ref.display_name), + subtype_id=note.id, + email_layout_xmlid='mail.mail_notification_light', + partner_ids=[(6, 0, partner.ids)]) + self = self.with_context(lang=saved_lang) + def action_send_mail(self): - active_record = self.env[self.res_model].browse(self.res_id) - note = self.env.ref('mail.mt_note') + note = self._get_note() signup_enabled = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope') == 'b2c' - if hasattr(active_record, 'access_token') and active_record.access_token or not signup_enabled: + if getattr(self.resource_ref, 'access_token', False) or not signup_enabled: partner_ids = self.partner_ids else: partner_ids = self.partner_ids.filtered(lambda x: x.user_ids) # if partner already user or record has access token send common link in batch to all user - for partner in self.partner_ids: - share_link = active_record.get_base_url() + active_record._get_share_url(redirect=True, pid=partner.id) - saved_lang = self.env.lang - self = self.with_context(lang=partner.lang) - template = self.env.ref('portal.portal_share_template', False) - active_record.message_post_with_view(template, - values={'partner': partner, 'note': self.note, 'record': active_record, - 'share_link': share_link}, - subject=_("You are invited to access %s", active_record.display_name), - subtype_id=note.id, - email_layout_xmlid='mail.mail_notification_light', - partner_ids=[(6, 0, partner.ids)]) - self = self.with_context(lang=saved_lang) + self._send_public_link(note, partner_ids) # when partner not user send individual mail with signup token - for partner in self.partner_ids - partner_ids: - # prepare partner for signup and send singup url with redirect url - partner.signup_get_auth_param() - share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.model)[partner.id] - saved_lang = self.env.lang - self = self.with_context(lang=partner.lang) - template = self.env.ref('portal.portal_share_template', False) - active_record.message_post_with_view(template, - values={'partner': partner, 'note': self.note, 'record': active_record, - 'share_link': share_link}, - subject=_("You are invited to access %s", active_record.display_name), - subtype_id=note.id, - email_layout_xmlid='mail.mail_notification_light', - partner_ids=[(6, 0, partner.ids)]) - self = self.with_context(lang=saved_lang) + self._send_signup_link(note, self.partner_ids - partner_ids) # subscribe all recipients so that they receive future communication (better than # using autofollow as more precise) - active_record.message_subscribe(partner_ids=self.partner_ids.ids) + self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index bde65068b5d..0c28fc67a61 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -33,6 +33,7 @@ 'views/project_update_templates.xml', 'views/project_project_stage_views.xml', 'wizard/project_share_wizard_views.xml', + 'views/project_collaborator_views.xml', 'views/project_views.xml', 'views/res_partner_views.xml', 'views/res_config_settings_views.xml', diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index cd04920d309..152e531f5f7 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -113,7 +113,6 @@ class ProjectCustomerPortal(CustomerPortal): cache_hashes=cache_hashes, action_name='project.project_sharing_project_task_action', project_id=project.id, - can_edit=project._check_project_sharing_access('edit'), user_companies={ 'current_company': project_company.id, 'allowed_companies': { diff --git a/addons/project/controllers/project_sharing_chatter.py b/addons/project/controllers/project_sharing_chatter.py index c29123bb216..c3e3bc2b9c2 100644 --- a/addons/project/controllers/project_sharing_chatter.py +++ b/addons/project/controllers/project_sharing_chatter.py @@ -23,7 +23,7 @@ class ProjectSharingChatter(PortalChatter): If it is the case, then we give the access token of the task. """ project_sudo = ProjectCustomerPortal._document_check_access(self, 'project.project', project_id, token) - can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access('comment') + can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access() if token: can_access &= consteq(project_sudo.access_token, token) else: diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index dae83cffa9e..2f66f8d42bc 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -6,7 +6,7 @@ from . import project_milestone from . import project_project_stage from . import project_task_recurrence from . import project -from . import project_sharing_access +from . import project_collaborator from . import project_update from . import res_config_settings from . import res_partner diff --git a/addons/project/models/project.py b/addons/project/models/project.py index a75561cf9cb..8f4a3b7c2d6 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -7,13 +7,12 @@ from collections import defaultdict from datetime import timedelta, datetime from random import randint -from odoo import api, fields, models, tools, SUPERUSER_ID, _ +from odoo import api, Command, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import UserError, ValidationError, AccessError -from odoo.osv import expression +from odoo.osv.expression import OR from .project_task_recurrence import DAYS, WEEKS from .project_update import STATUS_COLOR -from .project_sharing_access import PROJECT_SHARING_ACCESS_MODE PROJECT_TASK_READABLE_FIELDS = { @@ -38,7 +37,6 @@ PROJECT_TASK_READABLE_FIELDS = { 'displayed_image_id', 'display_name', 'priority', - 'project_sharing_access_mode', } PROJECT_TASK_WRITABLE_FIELDS = { @@ -283,7 +281,8 @@ class Project(models.Model): tag_ids = fields.Many2many('project.tags', relation='project_project_project_tags_rel', string='Tags') # Project Sharing fields - project_sharing_access_ids = fields.One2many('project.sharing.access', 'project_id', 'Project Sharing Access', copy=False) + collaborator_ids = fields.One2many('project.collaborator', 'project_id', string='Collaborators', copy=False) + collaborator_count = fields.Integer('# Collaborators', compute='_compute_collaborator_count') # rating fields rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True) @@ -397,6 +396,18 @@ class Project(models.Model): else: project.alias_value = "%s@%s" % (project.alias_name, project.alias_domain) + @api.depends('collaborator_ids', 'privacy_visibility') + def _compute_collaborator_count(self): + project_sharings = self.filtered(lambda project: project.privacy_visibility == 'portal') + collaborator_read_group = self.env['project.collaborator'].read_group( + [('project_id', 'in', project_sharings.ids)], + ['project_id'], + ['project_id'], + ) + collaborator_count_by_project = {res['project_id'][0]: res['project_id_count'] for res in collaborator_read_group} + for project in self: + project.collaborator_count = collaborator_count_by_project.get(project.id, 0) + @api.model def _map_tasks_default_valeus(self, task, project): """ get the default value for the copied task on project duplication """ @@ -700,25 +711,6 @@ class Project(models.Model): }) project.write({'analytic_account_id': analytic_account.id}) - def _check_project_sharing_access(self, access_mode='read'): - self.ensure_one() - if access_mode not in map(lambda access: access[0], PROJECT_SHARING_ACCESS_MODE): - raise ValidationError('The %s does not exists in project sharing accesses.' % access_mode) - if self.privacy_visibility != 'portal' or self.env.user._is_public(): - return False - if self.env.user.has_group('base.group_portal'): - # check in project sharing access model of the project - project_sharing_access = self.project_sharing_access_ids.filtered(lambda access: access.user_id == self.env.user) - if not project_sharing_access: - return False - if access_mode == 'edit': - return project_sharing_access.access_mode == 'edit' - elif access_mode == 'comment': - return project_sharing_access.access_mode in ('comment', 'edit') - else: - return True - return self.env.user.has_group('base.group_user') - # --------------------------------------------------- # Rating business # --------------------------------------------------- @@ -750,6 +742,35 @@ class Project(models.Model): project.message_unsubscribe(partner_ids=portal_users.partner_id.ids) project.mapped('tasks')._change_project_privacy_visibility() + # --------------------------------------------------- + # Project sharing + # --------------------------------------------------- + def _check_project_sharing_access(self): + self.ensure_one() + if self.privacy_visibility != 'portal': + return False + if self.env.user.has_group('base.group_portal'): + return self.env.user.partner_id in self.collaborator_ids.partner_id + return self.env.user.has_group('base.group_user') + + def _add_collaborators(self, partners): + self.ensure_one() + user_group_id = self.env['ir.model.data']._xmlid_to_res_id('base.group_user') + all_collaborators = self.collaborator_ids.partner_id + new_collaborators = partners.filtered( + lambda partner: + partner not in all_collaborators + and (not partner.user_ids or user_group_id not in partner.user_ids[0].groups_id.ids) + ) + if not new_collaborators: + # Then we have nothing to do + return + self.write({'collaborator_ids': [ + Command.create({ + 'partner_id': collaborator.id, + }) for collaborator in new_collaborators], + }) + class Task(models.Model): _name = "project.task" _description = "Task" @@ -880,9 +901,6 @@ class Task(models.Model): allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies') depend_on_ids = fields.Many2many('project.task', relation="task_dependencies_rel", column1="task_id", column2="depends_on_id", string="Blocked By", domain="[('allow_task_dependencies', '=', True), ('id', '!=', id)]") - # Project Sharing fields - project_sharing_access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, compute='_compute_project_sharing_access_mode') - # recurrence fields allow_recurring_tasks = fields.Boolean(related='project_id.allow_recurring_tasks') recurring_task = fields.Boolean(string="Recurrent") @@ -1189,26 +1207,6 @@ class Task(models.Model): else: task.stage_id = False - @api.depends_context('uid') - @api.depends('project_id.project_sharing_access_ids', 'project_id.privacy_visibility') - def _compute_project_sharing_access_mode(self): - if self.env.user._is_public(): - self.project_sharing_access_mode = False - return - portal_user = self.user_has_groups('base.group_portal') - project_sharings = self.project_id.filtered(lambda project: project.privacy_visibility == 'portal') - if portal_user: - project_sharing_access_read = self.env['project.sharing.access'].search_read( - [('user_id', '=', self.env.user.id), ('project_id', 'in', project_sharings.ids)], - ['project_id', 'access_mode'] - ) - project_sharing_access_dict = {res['project_id'][0]: res['access_mode'] for res in project_sharing_access_read} - for task in self: - if not portal_user: - task.project_sharing_access_mode = task.project_id in project_sharings and 'edit' - continue - task.project_sharing_access_mode = project_sharing_access_dict.get(task.project_id.id, False) - @api.constrains('recurring_task') def _check_recurring_task(self): if self.filtered(lambda task: task.parent_id and task.recurring_task): @@ -1497,7 +1495,7 @@ class Task(models.Model): recurrence_domain = [] if recurrence_update == 'subsequent': for task in self: - recurrence_domain = expression.OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]]) + recurrence_domain = OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]]) else: recurrence_domain = [('recurrence_id', 'in', self.recurrence_id.ids)] tasks |= self.env['project.task'].search(recurrence_domain) diff --git a/addons/project/models/project_collaborator.py b/addons/project/models/project_collaborator.py new file mode 100644 index 00000000000..4268a62f705 --- /dev/null +++ b/addons/project/models/project_collaborator.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ProjectCollaborator(models.Model): + _name = 'project.collaborator' + _description = 'Collaborators in project shared' + + project_id = fields.Many2one('project.project', 'Project Shared', domain=[('privacy_visibility', '=', 'portal')], required=True) + partner_id = fields.Many2one('res.partner', 'Collaborator', required=True) + + _sql_constraints = [ + ('unique_collaborator', 'UNIQUE(project_id, partner_id)', 'A collaborator cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'), + ] + + @api.model_create_multi + def create(self, vals_list): + project_collaborators = super().create(vals_list) + non_authenticated_collaborator = project_collaborators.partner_id.filtered(lambda partner: not partner.user_ids) + non_authenticated_collaborator._create_portal_users() + return project_collaborators diff --git a/addons/project/models/project_sharing_access.py b/addons/project/models/project_sharing_access.py deleted file mode 100644 index c164f89681e..00000000000 --- a/addons/project/models/project_sharing_access.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models - - -PROJECT_SHARING_ACCESS_MODE = [ - ('read', 'Read'), - ('comment', 'Comment'), - ('edit', 'Edit'), -] - - -class ProjectSharingAccess(models.Model): - _name = 'project.sharing.access' - _description = 'Project Sharing Access' - - project_id = fields.Many2one('project.project', 'Project Shared', required=True) - user_id = fields.Many2one('res.users', string='Portal Users', domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_portal').id)], required=True) - access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, string='Access Mode', required=True) - - _sql_constraints = [ - ('unique_user', 'UNIQUE(project_id, user_id)', 'A user cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'), - ] diff --git a/addons/project/models/res_partner.py b/addons/project/models/res_partner.py index d7646c8d411..290e92d8474 100644 --- a/addons/project/models/res_partner.py +++ b/addons/project/models/res_partner.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models +from odoo.tools import email_normalize class ResPartner(models.Model): @@ -28,3 +29,19 @@ class ResPartner(models.Model): if partner in self: partner.task_count += group['partner_id_count'] partner = partner.parent_id + + def _create_portal_users(self): + partners_without_user = self.filtered(lambda partner: not partner.user_ids) + if not partners_without_user: + return self.env['res.users'] + created_users = self.env['res.users'] + for partner in partners_without_user: + created_users += self.env['res.users'].with_context(no_reset_password=True)._create_user_from_template({ + 'email': email_normalize(partner.email), + 'login': email_normalize(partner.email), + 'partner_id': partner.id, + 'company_id': self.env.company.id, + 'company_ids': [(6, 0, self.env.company.ids)], + 'active': True, + }) + return created_users diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 0f06777ee58..f9637300734 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -36,7 +36,6 @@ access_project_milestone_user,project.milestone.user,model_project_milestone,bas access_project_milestone_portal,project.milestone.portal,model_project_milestone,base.group_portal,1,0,0,0 access_project_milestone_project_user,project.milestone.project.user,model_project_milestone,project.group_project_user,1,0,0,0 access_project_milestone_project_manager,project.milestone.project.manager,model_project_milestone,project.group_project_manager,1,1,1,1 -access_project_share_project_manager,project.share.wizard.project.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0 -access_project_share_wizard_line_project_manager,project.share.wizard.line.project.manager,model_project_share_wizard_line,project.group_project_manager,1,1,1,0 -access_project_sharing_access_project_manager,project.sharing.access.project.manager,model_project_sharing_access,project.group_project_manager,1,1,1,1 -access_project_sharing_access_project_portal,project.sharing.access.project.portal,model_project_sharing_access,base.group_portal,1,0,0,0 +access_project_collaborator_manager,project.collaborator.manager,model_project_collaborator,project.group_project_manager,1,1,1,1 +access_project_collaborator_portal,project.collaborator.portal,model_project_collaborator,base.group_portal,1,0,0,0 +access_project_share_manager,project.share.wizard.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0 diff --git a/addons/project/security/project_security.xml b/addons/project/security/project_security.xml index e66dde35ba5..ebe00e1ca0f 100644 --- a/addons/project/security/project_security.xml +++ b/addons/project/security/project_security.xml @@ -145,14 +145,13 @@ '|', ('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), ('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), - ('project_id.project_sharing_access_ids.user_id', 'in', [user.id]), - ('project_id.project_sharing_access_ids.access_mode', '=', 'edit'), + ('project_id.collaborator_ids.partner_id', 'in', [user.partner_id.id]), ] + - @@ -183,12 +182,5 @@ - - Project Sharing Access: Portal user can read its own access - - [('user_id', '=', user.id)] - - - diff --git a/addons/project/static/src/project_sharing/project_sharing.js b/addons/project/static/src/project_sharing/project_sharing.js index cc59095bdfe..cf1affa05eb 100644 --- a/addons/project/static/src/project_sharing/project_sharing.js +++ b/addons/project/static/src/project_sharing/project_sharing.js @@ -56,15 +56,13 @@ export class ProjectSharingWebClient extends Component { } async _showView() { - const { action_name, project_id, can_edit } = session; + const { action_name, project_id } = session; await this.actionService.doAction( action_name, { clearBreadcrumbs: true, additionalContext: { active_id: project_id, - create: can_edit, - edit: can_edit, } } ); diff --git a/addons/project/tests/test_project_sharing.py b/addons/project/tests/test_project_sharing.py index a39b81b913f..04af79e5333 100644 --- a/addons/project/tests/test_project_sharing.py +++ b/addons/project/tests/test_project_sharing.py @@ -3,6 +3,7 @@ from odoo import Command from odoo.exceptions import AccessError +from odoo.tests import tagged from odoo.tests.common import Form from .test_project_base import TestProjectCommon @@ -18,6 +19,13 @@ class TestProjectSharingCommon(TestProjectCommon): (0, 0, {'name': 'To Do', 'sequence': 1}), (0, 0, {'name': 'Done', 'sequence': 10}), ] + + cls.partner_portal = cls.env['res.partner'].create({ + 'name': 'Chell Gladys', + 'email': 'chell@gladys.portal', + 'company_id': False, + 'user_ids': [Command.link(cls.user_portal.id)]}) + cls.project_cows = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ 'name': 'Cows', 'privacy_visibility': 'portal', @@ -49,16 +57,7 @@ class TestProjectSharingCommon(TestProjectCommon): view="project.project_sharing_project_task_view_form" ) - def get_project_sharing_access_for_user(self, with_user, project): - project_with_user = project.with_user(with_user or self.env.user) - return { - 'read': project_with_user._check_project_sharing_access(), - 'comment': project_with_user._check_project_sharing_access('comment'), - 'edit': project_with_user._check_project_sharing_access('edit') - } - - - +@tagged('project_sharing') class TestProjectSharing(TestProjectSharingCommon): def test_project_share_wizard(self): @@ -73,95 +72,30 @@ class TestProjectSharing(TestProjectSharingCommon): project_share_wizard = self.env['project.share.wizard'].create({ 'res_model': 'project.project', 'res_id': self.project_portal.id, + 'access_mode': 'edit', }) - self.assertFalse(project_share_wizard.line_ids, 'No access rights should be given to a portal user in this project.') + self.assertFalse(project_share_wizard.partner_ids, 'No collaborator should be in the wizard.') self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have accessed in project sharing views.') - project_share_wizard.write({'line_ids': [ - Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - }), - ]}) - project_share_wizard.action_confirm_access() - self.assertEqual(len(self.project_portal.project_sharing_access_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.') + project_share_wizard.write({'partner_ids': [Command.link(self.user_portal.partner_id.id)]}) + project_share_wizard.action_send_mail() + self.assertEqual(len(self.project_portal.collaborator_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.') self.assertDictEqual({ - 'user_id': self.project_portal.project_sharing_access_ids.user_id, - 'access_mode': self.project_portal.project_sharing_access_ids.access_mode, - 'project_id': self.project_portal.project_sharing_access_ids.project_id, + 'partner_id': self.project_portal.collaborator_ids.partner_id, + 'project_id': self.project_portal.collaborator_ids.project_id, }, { - 'user_id': self.user_portal, - 'access_mode': 'read', + 'partner_id': self.user_portal.partner_id, 'project_id': self.project_portal, }, 'The access rights added should be the read access for the portal project for Chell Gladys.') - self.assertTrue(self.project_portal._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.') - self.assertTrue(self.user_portal.partner_id in self.project_portal.message_partner_ids) + self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.') def test_project_sharing_access(self): - """ Check the _check_project_sharing_access returns the expected value for all access mode defined in project sharing. - """ - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_public, self.project_portal), - { - 'read': False, - 'comment': False, - 'edit': False, - }, - 'The public user should not have any access to project sharing feature of the portal project.' - ) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_projectuser, self.project_portal), - { - 'read': True, - 'comment': True, - 'edit': True, - }, - 'The internal user should have all accesses to project sharing feature of the portal project.' - ) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': False, - 'comment': False, - 'edit': False, - }, - 'The portal user should not have any access to project sharing feature of the portal project.' - ) - self.project_portal.write({ - 'project_sharing_access_ids': [Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - })], - }) - project_sharing_access = self.project_portal.project_sharing_access_ids - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': True, - 'comment': False, - 'edit': False, - }, - 'The portal user should have only the read access to project sharing feature of the portal project.' - ) - project_sharing_access.write({'access_mode': 'comment'}) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': True, - 'comment': True, - 'edit': False, - }, - 'The portal user should can read and comment the tasks of the portal project in project sharing views.' - ) - project_sharing_access.write({'access_mode': 'edit'}) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': True, - 'comment': True, - 'edit': True, - }, - 'The portal user should can read, comment and edit the tasks of the portal project in project sharing views.' - ) + """ Check if the different user types can access to project sharing feature as expected. """ + with self.assertRaises(AccessError, msg='The public user should not have any access to project sharing feature of the portal project.'): + self.project_portal.with_user(self.user_public)._check_project_sharing_access() + self.assertTrue(self.project_portal.with_user(self.user_projectuser)._check_project_sharing_access(), 'The internal user should have all accesses to project sharing feature of the portal project.') + self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have any access to project sharing feature of the portal project.') + self.project_portal.write({'collaborator_ids': [Command.create({'partner_id': self.user_portal.partner_id.id})]}) + self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user can access to project sharing feature of the portal project.') def test_create_task_in_project_sharing(self): """ Test when portal user creates a task in project sharing views. @@ -174,29 +108,16 @@ class TestProjectSharing(TestProjectSharingCommon): 3.1) Try to change the project of the new task with this user. """ # 1) Give the 'read' access mode to a portal user in a project and try to create task with this user. + with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): + with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: + form.name = 'Test' + task = form.save() + self.project_portal.write({ - 'project_sharing_access_ids': [ - Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - }), + 'collaborator_ids': [ + Command.create({'partner_id': self.user_portal.partner_id.id}), ], }) - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() - - # 2) Give the 'comment' access mode to a portal user in a project and try to create task with this user. - project_sharing_access = self.project_portal.project_sharing_access_ids - project_sharing_access.write({'access_mode': 'comment'}) - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() - - # 3) Give the 'edit' access mode to a portal user in a project and try to create task with this user. - project_sharing_access.write({'access_mode': 'edit'}) with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: form.name = 'Test' task = form.save() @@ -219,32 +140,21 @@ class TestProjectSharing(TestProjectSharingCommon): 3.1) Try to change the project of the new task with this user. """ # 1) Give the 'read' access mode to a portal user in a project and try to create task with this user. + with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): + with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form: + form.name = 'Test' + task = form.save() + project_share_wizard = self.env['project.share.wizard'].create({ + 'access_mode': 'edit', 'res_model': 'project.project', 'res_id': self.project_cows.id, - 'line_ids': [ - Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - }), + 'partner_ids': [ + Command.link(self.user_portal.partner_id.id), ], }) - project_share_wizard.action_confirm_access() - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() + project_share_wizard.action_send_mail() - # 2) Give the 'comment' access mode to a portal user in a project and try to create task with this user. - project_sharing_access = self.project_cows.project_sharing_access_ids - project_sharing_access.write({'access_mode': 'comment'}) - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() - - # 3) Give the 'edit' access mode to a portal user in a project and try to create task with this user. - project_sharing_access.write({'access_mode': 'edit'}) with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id, 'uid': self.user_portal.id}), self.user_portal) as form: form.name = 'Test' task = form.save() diff --git a/addons/project/views/project_collaborator_views.xml b/addons/project/views/project_collaborator_views.xml new file mode 100644 index 00000000000..09ebf0173d4 --- /dev/null +++ b/addons/project/views/project_collaborator_views.xml @@ -0,0 +1,53 @@ + + + + + project.collaborator.view.form + project.collaborator + +
+ + + + + + +
+
+
+ + + project.collaborator.view.tree + project.collaborator + + + + + + + + + + project.collaborator.view.search + project.collaborator + + + + + + + + + + + + + + Project Collaborators + project.collaborator + tree,form + [('project_id', '=', active_id)] + + + +
diff --git a/addons/project/views/project_sharing_views.xml b/addons/project/views/project_sharing_views.xml index 6426e3c8a75..ea603592e99 100644 --- a/addons/project/views/project_sharing_views.xml +++ b/addons/project/views/project_sharing_views.xml @@ -133,8 +133,7 @@
- - +
@@ -145,7 +144,7 @@

- +

@@ -196,7 +195,7 @@
-
+
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 47661621d4e..cf46269bf37 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -264,8 +264,10 @@
-
@@ -312,6 +314,16 @@
+
@@ -640,7 +652,7 @@
diff --git a/addons/project/wizard/project_share_wizard.py b/addons/project/wizard/project_share_wizard.py index 635a87c45cb..113f0252023 100644 --- a/addons/project/wizard/project_share_wizard.py +++ b/addons/project/wizard/project_share_wizard.py @@ -1,95 +1,47 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, Command, fields, models, _ -from odoo.exceptions import ValidationError, UserError - -from ..models.project_sharing_access import PROJECT_SHARING_ACCESS_MODE +from odoo import api, fields, models class ProjectShareWizard(models.TransientModel): _name = 'project.share.wizard' _inherit = 'portal.share' - _description = 'Project Share' + _description = 'Project Sharing' @api.model - def default_get(self, fields_list): - result = super().default_get(fields_list) - if result['res_model'] and result['res_id']: - project = self.env[result['res_model']].browse(result['res_id']) + def default_get(self, fields): + result = super().default_get(fields) + if not result.get('access_mode'): result.update( - line_ids=[ - Command.create( - {'user_id': access.user_id, 'access_mode': access.access_mode} - ) for access in project.project_sharing_access_ids - ], + access_mode='read', + display_access_mode=True, ) return result - partner_ids = fields.Many2many(compute="_compute_partner_ids", readonly=False, store=True, required=False) - line_ids = fields.One2many('project.share.wizard.line', 'project_share_id') - has_access_changed = fields.Boolean(compute='_compute_has_access_changed') + @api.model + def _selection_target_model(self): + return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([('model', '=', 'project.project')])] - @api.depends('line_ids.user_id') - def _compute_partner_ids(self): - for project_share_wizard in self: - project_share_wizard.partner_ids |= project_share_wizard.line_ids.user_id.partner_id + access_mode = fields.Selection([('read', 'Readonly'), ('edit', 'Edit')]) + display_access_mode = fields.Boolean() - @api.depends('line_ids.user_id', 'line_ids.access_mode') - def _compute_has_access_changed(self): - project_sharing_access_read = self.env['project.sharing.access'].search_read([('project_id', '=', self.res_id)], ['user_id', 'access_mode']) - project_sharing_access_dict = {res['user_id'][0]: res['access_mode'] for res in project_sharing_access_read} - for project_share in self: - if len(project_share.line_ids) != len(project_sharing_access_dict): - project_share.has_access_changed = True - continue - has_access_changed = False - for line in project_share.line_ids: - access_mode = project_sharing_access_dict.get(line.user_id.id) - if not access_mode or access_mode != line.access_mode: - has_access_changed = True - break - project_share.has_access_changed = has_access_changed + @api.depends('res_model', 'res_id') + def _compute_resource_ref(self): + for wizard in self: + if wizard.res_model and wizard.res_model == 'project.project': + wizard.resource_ref = '%s,%s' % (wizard.res_model, wizard.res_id or 0) + else: + wizard.resource_ref = None - def action_send_mail_and_confirm_access(self): - if not self.partner_ids: - raise UserError(_('Impossible to send email, no recipients are mentionned.')) - self.action_confirm_access() - return self.action_send_mail() - - def action_confirm_access(self): - project = self.env[self.res_model].browse(self.res_id) - if not project or not project.exists(): - raise ValidationError(_('Impossible to confirm the project sharing access, the project is not found or project sharing is disabled for this project.')) - project_sharing_access_dict = {access.user_id.id: access for access in project.project_sharing_access_ids} - project_sharing_access_vals_list = [ - Command.delete(access.id) - for user_id, access in project_sharing_access_dict.items() - if user_id not in self.line_ids.user_id.ids - ] - for share_access in self.line_ids: - project_sharing_access = project_sharing_access_dict.get(share_access.user_id.id) - if not project_sharing_access: - project_sharing_access_vals_list.append( - Command.create( - {'user_id': share_access.user_id.id, 'access_mode': share_access.access_mode} - ) - ) - elif project_sharing_access.access_mode != share_access.access_mode: - project_sharing_access_vals_list.append(Command.update(project_sharing_access.id, {'access_mode': share_access.access_mode})) - project.write({'project_sharing_access_ids': project_sharing_access_vals_list}) - project.message_subscribe(self.line_ids.user_id.partner_id.ids) - return {'type': 'ir.actions.act_window_close'} - - -class ProjectShareAccessWizard(models.TransientModel): - _name = 'project.share.wizard.line' - _description = 'Project Share Access' - - project_share_id = fields.Many2one('project.share.wizard', required=True, ondelete='cascade') - user_id = fields.Many2one('res.users', string='Portal Users', domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_portal').id)], required=True) - access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, string='Access Mode', required=True) - - _sql_constraints = [ - ('uniqueness_user', 'UNIQUE(project_share_id, user_id)', 'An user cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'), - ] + def action_send_mail(self): + self.ensure_one() + if self.access_mode == 'edit': + portal_partners = self.partner_ids.filtered('user_ids') + note = self._get_note() + self.resource_ref._add_collaborators(self.partner_ids) + self._send_public_link(note, portal_partners) + self._send_signup_link(note, partners=self.partner_ids - portal_partners) + self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids) + return {'type': 'ir.actions.act_window_close'} + return super().action_send_mail() diff --git a/addons/project/wizard/project_share_wizard_views.xml b/addons/project/wizard/project_share_wizard_views.xml index ee8bdb230c6..786a7383059 100644 --- a/addons/project/wizard/project_share_wizard_views.xml +++ b/addons/project/wizard/project_share_wizard_views.xml @@ -1,46 +1,43 @@ - - project.share.wizard + + project.share.wizard.view.form project.share.wizard - + + + + - - - + + + + - +
+
+
- - - - - - - -
- -
- + Share Project project.share.wizard - + form new