[IMP] project: select access rights in project sharing

Before this commit, when the portal user can access to project sharing
views, he can just read. If we just add ir.rule to allow the edition,
the portal could create/edit. We have to allow the project manager to
define the access rights of portal users when he want to share them a
project with the project sharing feature.

This commit adds 3 access modes portal users in project sharing views.
1. **Readonly**: the portal can only see the tasks in the different views.
2. **Comment**: the portal can use the chatter in task form view.
3. **Edit**: the portal user can create and edit tasks.

task-2379518

closes #73341

Co-Authored-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Xavier BOL (xbo)
2021-08-26 14:59:33 +02:00
co-authored by Yannick Tivisse
parent 716f47d14b
commit 5ea5ddafea
13 changed files with 252 additions and 20 deletions
+1
View File
@@ -32,6 +32,7 @@
'views/project_update_views.xml',
'views/project_update_templates.xml',
'views/project_project_stage_views.xml',
'wizard/project_share_wizard_views.xml',
'views/project_views.xml',
'views/res_partner_views.xml',
'views/res_config_settings_views.xml',
+2 -2
View File
@@ -104,8 +104,7 @@ class ProjectCustomerPortal(CustomerPortal):
@http.route("/embed/project/<int:project_id>", type="http", auth="user", methods=['GET'])
def render_project_backend_view(self, project_id):
project = request.env['project.project'].sudo().browse(project_id)
if not project.exists():
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
return request.not_found()
session_info = request.env['ir.http'].session_info()
@@ -124,6 +123,7 @@ class ProjectCustomerPortal(CustomerPortal):
cache_hashes=cache_hashes,
action_name='project.project_sharing_project_task_action',
project_id=project.id,
can_edit=project.user_can_edit,
user_companies={
'current_company': project_company.id,
'allowed_companies': {
+1
View File
@@ -6,6 +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_update
from . import res_config_settings
from . import res_partner
+53 -6
View File
@@ -9,10 +9,11 @@ from random import randint
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import UserError, ValidationError, AccessError
from odoo.osv.expression import OR
from odoo.osv import expression
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 = {
@@ -37,6 +38,7 @@ PROJECT_TASK_READABLE_FIELDS = {
'displayed_image_id',
'display_name',
'priority',
'project_sharing_access_mode',
}
PROJECT_TASK_WRITABLE_FIELDS = {
@@ -280,6 +282,9 @@ class Project(models.Model):
allow_task_dependencies = fields.Boolean('Task Dependencies', default=lambda self: self.env.user.has_group('project.group_project_task_dependencies'))
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)
# rating fields
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
rating_active = fields.Boolean('Customer Ratings', default=lambda self: self.env.user.has_group('project.group_project_rating'))
@@ -695,6 +700,25 @@ 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
# ---------------------------------------------------
@@ -856,6 +880,9 @@ 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")
@@ -1162,6 +1189,26 @@ 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):
@@ -1241,10 +1288,10 @@ class Task(models.Model):
readable_fields = self.SELF_READABLE_FIELDS
public_fields = {field_name: description for field_name, description in fields.items() if field_name in readable_fields}
writable_fields = self.SELF_WRITABLE_FIELDS
for field_name, description in public_fields.items():
if field_name not in writable_fields and not description.get('readonly', False):
# If the field is not in Writable fields and it is not readonly then we force the readonly to True
writable_fields = self.SELF_WRITABLE_FIELDS
for field_name, description in public_fields.items():
if field_name not in writable_fields and not description.get('readonly', False):
# If the field is not in Writable fields and it is not readonly then we force the readonly to True
description['readonly'] = True
return public_fields
@@ -1412,7 +1459,7 @@ class Task(models.Model):
recurrence_domain = []
if recurrence_update == 'subsequent':
for task in self:
recurrence_domain = OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]])
recurrence_domain = expression.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)
@@ -0,0 +1,24 @@
# -*- 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.'),
]
@@ -36,3 +36,7 @@ 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
36 access_project_milestone_portal project.milestone.portal model_project_milestone base.group_portal 1 0 0 0
37 access_project_milestone_project_user project.milestone.project.user model_project_milestone project.group_project_user 1 0 0 0
38 access_project_milestone_project_manager project.milestone.project.manager model_project_milestone project.group_project_manager 1 1 1 1
39 access_project_share_project_manager project.share.wizard.project.manager model_project_share_wizard project.group_project_manager 1 1 1 0
40 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
41 access_project_sharing_access_project_manager project.sharing.access.project.manager model_project_sharing_access project.group_project_manager 1 1 1 1
42 access_project_sharing_access_project_portal project.sharing.access.project.portal model_project_sharing_access base.group_portal 1 0 0 0
+32 -6
View File
@@ -123,14 +123,33 @@
<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'),
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
]</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="project_task_rule_portal_project_sharing" model="ir.rule">
<field name="name">Project/Task: portal users: portal user can edit with project sharing feature</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]),
('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'),
]</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
@@ -162,5 +181,12 @@
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
</record>
<record id="project_sharing_access_portal" model="ir.rule">
<field name="name">Project Sharing Access: Portal user can read its own access</field>
<field name="model_id" ref="project.model_project_sharing_access" />
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</data>
</odoo>
@@ -56,13 +56,15 @@ export class ProjectSharingWebClient extends Component {
}
async _showView() {
const { action_name, project_id } = session;
const { action_name, project_id, can_edit } = session;
await this.actionService.doAction(
action_name,
{
clearBreadcrumbs: true,
additionalContext: {
active_id: project_id,
create: can_edit,
edit: can_edit,
}
}
);
@@ -132,7 +132,8 @@
<field name="arch" type="xml">
<form string="Project Sharing: Task" class="o_form_project_tasks">
<header>
<field name="stage_id" widget="statusbar" options="{'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}" />
<field name="project_sharing_access_mode" invisible="1"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)], 'readonly': [('project_sharing_access_mode', '!=', 'edit')]}" />
</header>
<sheet string="Task">
<div class="oe_button_box" name="button_box">
@@ -143,7 +144,7 @@
<h1 class="d-flex flex-row justify-content-between">
<field name="priority" widget="priority" class="mr-3"/>
<field name="name" class="o_task_name text-truncate" placeholder="Task Title..."/>
<field name="kanban_state" widget="state_selection" class="ml-auto" readonly="1"/>
<field name="kanban_state" widget="state_selection" class="ml-auto" attrs="{'readonly': [('project_sharing_access_mode', '!=', 'edit')]}"/>
</h1>
</div>
<group>
@@ -194,7 +195,7 @@
</page>
</notebook>
</sheet>
<div class="oe_chatter"/>
<div class="oe_project_sharing_chatter" attrs="{'invisible': [('project_sharing_access_mode', 'not in', ['comment', 'edit'])]}"/>
</form>
</field>
</record>
+2 -2
View File
@@ -264,7 +264,7 @@
<field name="arch" type="xml">
<form string="Project" delete="0">
<header>
<button name="%(portal.portal_share_action)d" string="Share" type="action" class="oe_highlight" groups="project.group_project_manager"
<button name="%(project.project_share_action)d" string="Share" type="action" class="oe_highlight" groups="project.group_project_manager"
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" groups="project.group_project_stages"/>
</header>
@@ -640,7 +640,7 @@
<ul class="oe_kanban_colorpicker" data-field="color" role="popup"/>
</div>
<div role="menuitem" class="col-4">
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(portal.portal_share_action)d" type="action">Share</a>
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_action)d" type="action">Share</a>
<a class="dropdown-item" role="menuitem" type="edit">Edit</a>
</div>
</div>
+1
View File
@@ -3,3 +3,4 @@
from . import project_delete_wizard
from . import project_task_type_delete
from . import project_share_wizard
@@ -0,0 +1,78 @@
# -*- 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
class ProjectShareWizard(models.TransientModel):
_name = 'project.share.wizard'
_inherit = 'portal.share'
_description = 'Project Share'
@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'])
result.update(
line_ids=[
Command.create(
{'user_id': access.user_id, 'access_mode': access.access_mode}
) for access in project.project_sharing_access_ids
],
)
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')
@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
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.'),
]
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_share_wizard" model="ir.ui.view">
<field name="name">project.share.wizard</field>
<field name="model">project.share.wizard</field>
<field name="arch" type="xml">
<form string="Share Document">
<p class="alert alert-warning" attrs="{'invisible': [('access_warning', '=', '')]}" role="alert"><field name="access_warning"/></p>
<group name="share_link">
<field name="res_model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="share_link" widget="CopyClipboardChar" options="{'string': 'Copy Link'}"/>
</group>
<group>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to share the document..."/>
</group>
<group>
<field name="note" placeholder="Add a note"/>
</group>
<group>
<field name="line_ids" nolabel="1">
<tree create="1" edit="1" delete="1" editable="bottom">
<field name="user_id" />
<field name="access_mode" />
</tree>
</field>
</group>
<footer>
<button string="Confirm Access" name="action_confirm_access" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('line_ids', '=', [])]}" type="object" class="btn-primary" data-hotkey="v"/>
<button string="Send" name="action_send_mail" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('line_ids', '!=', [])]}" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Send &amp; Confirm" name="action_send_mail_and_confirm_access" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('line_ids', '=', [])]}" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-default" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="project_share_action" model="ir.actions.act_window">
<field name="name">Share Project</field>
<field name="res_model">project.share.wizard</field>
<field name="binding_model_id" ref="model_project_share_wizard"/>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>