[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
This commit is contained in:
Xavier BOL (xbo)
2021-08-26 16:57:23 +02:00
parent 0de3f3ace5
commit a28cc6333a
19 changed files with 314 additions and 362 deletions
@@ -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]),
]</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
+58 -32
View File
@@ -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'}
+1
View File
@@ -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',
-1
View File
@@ -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': {
@@ -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:
+1 -1
View File
@@ -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
+46 -48
View File
@@ -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)
@@ -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
@@ -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.'),
]
+17
View File
@@ -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
+3 -4
View File
@@ -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
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 access_project_collaborator_manager project.share.wizard.project.manager project.collaborator.manager model_project_share_wizard model_project_collaborator project.group_project_manager 1 1 1 0 1
40 access_project_share_wizard_line_project_manager access_project_collaborator_portal project.share.wizard.line.project.manager project.collaborator.portal model_project_share_wizard_line model_project_collaborator project.group_project_manager base.group_portal 1 1 0 1 0 0
41 access_project_sharing_access_project_manager access_project_share_manager project.sharing.access.project.manager project.share.wizard.manager model_project_sharing_access model_project_share_wizard project.group_project_manager 1 1 1 1 0
access_project_sharing_access_project_portal project.sharing.access.project.portal model_project_sharing_access base.group_portal 1 0 0 0
+2 -10
View File
@@ -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]),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<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>
<record model="ir.rule" id="update_comp_rule">
@@ -183,12 +182,5 @@
<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,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,
}
}
);
+41 -131
View File
@@ -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()
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_collaborator_view_form" model="ir.ui.view">
<field name="name">project.collaborator.view.form</field>
<field name="model">project.collaborator</field>
<field name="arch" type="xml">
<form string="Project Collaborator">
<sheet>
<group>
<field name="partner_id" options="{'no_create': True}" />
<field name="project_id" options="{'no_create': True}" />
</group>
</sheet>
</form>
</field>
</record>
<record id="project_sharing_access_view_tree" model="ir.ui.view">
<field name="name">project.collaborator.view.tree</field>
<field name="model">project.collaborator</field>
<field name="arch" type="xml">
<tree string="Project Collaborators" edit="1" editable="bottom">
<field name="partner_id" options="{'no_create': True}"/>
<field name="project_id" options="{'no_create': True}"/>
</tree>
</field>
</record>
<record id="project_collaborator_view_search" model="ir.ui.view">
<field name="name">project.collaborator.view.search</field>
<field name="model">project.collaborator</field>
<field name="arch" type="xml">
<search>
<field name="partner_id" />
<field name="project_id" />
<group expand="0" string="Group By">
<filter name="project" string="Project" context="{'group_by': 'project_id'}" />
<filter name="collaborator" string="Collaborator" context="{'group_by': 'partner_id'}" />
</group>
</search>
</field>
</record>
<record id="project_collaborator_action" model="ir.actions.act_window">
<field name="name">Project Collaborators</field>
<field name="res_model">project.collaborator</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('project_id', '=', active_id)]</field>
<field name="search_view_id" ref="project_collaborator_view_search"/>
</record>
</odoo>
@@ -133,8 +133,7 @@
<field name="arch" type="xml">
<form string="Project Sharing: Task" class="o_form_project_tasks">
<header>
<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')]}" />
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}" />
</header>
<sheet string="Task">
<div class="oe_button_box" name="button_box">
@@ -145,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" attrs="{'readonly': [('project_sharing_access_mode', '!=', 'edit')]}"/>
<field name="kanban_state" widget="state_selection" class="ml-auto"/>
</h1>
</div>
<group>
@@ -196,7 +195,7 @@
</page>
</notebook>
</sheet>
<div class="oe_project_sharing_chatter" attrs="{'invisible': [('project_sharing_access_mode', 'not in', ['comment', 'edit'])]}"/>
<div class="oe_chatter"/>
</form>
</field>
</record>
+15 -3
View File
@@ -264,8 +264,10 @@
<field name="arch" type="xml">
<form string="Project" delete="0">
<header>
<button name="%(project.project_share_action)d" string="Share" type="action" class="oe_highlight" groups="project.group_project_manager"
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}"/>
<button name="%(project.project_share_wizard_action)d" string="Share Readonly" type="action" class="oe_highlight" groups="project.group_project_manager"
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}" context="{'default_access_mode': 'read'}"/>
<button name="%(project.project_share_wizard_action)d" string="Share Editable" type="action" class="oe_highlight" groups="project.group_project_manager"
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}" context="{'default_access_mode': 'edit'}"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" groups="project.group_project_stages"/>
</header>
<sheet string="Project">
@@ -312,6 +314,16 @@
<field name="last_update_status" widget="status_with_color" options="{'color_field': 'last_update_color', 'no_quick_edit': 1}"/>
</div>
</button>
<button class="oe_stat_button" name="%(project.project_collaborator_action)d" type="action" icon="fa-users" groups="project.group_project_manager">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="collaborator_count" nolabel="1"/>
</span>
<span class="o_stat_text">
Collaborators
</span>
</div>
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title">
@@ -640,7 +652,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="%(project.project_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_wizard_action)d" type="action">Share</a>
<a class="dropdown-item" role="menuitem" type="edit">Edit</a>
</div>
</div>
+30 -78
View File
@@ -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()
@@ -1,46 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_share_wizard" model="ir.ui.view">
<field name="name">project.share.wizard</field>
<record id="project_share_wizard_view_form" model="ir.ui.view">
<field name="name">project.share.wizard.view.form</field>
<field name="model">project.share.wizard</field>
<field name="arch" type="xml">
<form string="Share Document">
<form string="Share Project">
<field name="res_model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="display_access_mode" invisible="1" />
<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"/>
<group attrs="{'invisible': [('display_access_mode', '=', False)]}">
<field class="flex-row" name="access_mode" widget="radio"/>
</group>
<group name="share_link" attrs="{'invisible': [('access_mode', '=', 'edit')]}">
<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..."/>
<div class="o_td_label">
<label for="partner_ids" string="Invite People" attrs="{'invisible': [('access_mode', '=', 'read')]}"/>
<label for="partner_ids" attrs="{'invisible': [('access_mode', '=', 'edit')]}"/>
</div>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to share the project..." nolabel="1"/>
</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>
<field name="has_access_changed" invisible="1" />
<button string="Confirm Access" name="action_confirm_access" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('has_access_changed', '=', False)]}" type="object" class="btn-primary" data-hotkey="v"/>
<button string="Send" name="action_send_mail" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('has_access_changed', '=', True)]}" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Send &amp; Confirm" name="action_send_mail_and_confirm_access" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('has_access_changed', '=', False)]}" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Send" name="action_send_mail" attrs="{'invisible': [('access_warning', '!=', '')]}" 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">
<record id="project_share_wizard_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="binding_model_id" ref="model_project_project"/>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>