Files
enterprise-addons/documents/wizard/documents_sharing.py
T

310 lines
15 KiB
Python

from odoo import _, api, Command, fields, models
class DocumentsSharing(models.TransientModel):
_name = 'documents.sharing'
_description = "Documents Sharing"
document_ids = fields.Many2many('documents.document', ondelete='cascade', readonly=True)
share_access_ids = fields.One2many('documents.sharing.access', 'documents_sharing_id', required=True)
# Rights edition
access_internal = fields.Selection('_get_role_options', string="Internal users", required=True)
access_internal_help = fields.Char(compute="_compute_access_internal_help")
access_via_link = fields.Selection('_get_role_options', string="Access through link", required=True)
access_via_link_help = fields.Char(compute="_compute_access_via_link_help")
access_via_link_mode = fields.Selection('_get_access_via_link_mode', string="Discoverable", required=True)
is_access_modified = fields.Boolean("Modified", compute="_compute_is_access_modified")
# Invitation
invite_role = fields.Selection(
[('view', 'Viewer'), ('edit', 'Editor')],
string='Role', default='view', required=True)
invite_notify = fields.Boolean("Notify", default=True)
invite_notify_message = fields.Html("Notification Message")
invite_partner_ids = fields.Many2many('res.partner')
# Additional readonly fields for displaying information
access_urls = fields.Char("Access URLs", compute="_compute_ui_values")
is_single = fields.Boolean("Single", compute="_compute_ui_values")
is_folder_only = fields.Boolean("Folder Only", compute="_compute_ui_values")
is_readonly = fields.Boolean("Readonly", compute="_compute_ui_values")
has_warning_link_with_more_rights = fields.Char(compute="_compute_has_warning_link_with_more_rights")
has_warning_partners_without_access = fields.Char(compute="_compute_has_warning_partners_without_access")
owner_id = fields.Many2one("res.users", string="Owner of all documents", compute="_compute_ui_values")
WRITE_VALUE_PREFIX = 'write_'
@api.model
def _add_write_options(self, selection_options):
"""Add write options to the given options list.
For each existing option, adds a corresponding option with the prefix WRITE_VALUE_PREFIX (except for "mixed").
Selecting a 'write_' option indicates that a change has been made."""
new_options = []
for code, label in selection_options:
new_options.append((code, label))
if code != 'mixed':
new_options.append((f'{self.WRITE_VALUE_PREFIX}{code}', label))
return new_options
@api.model
def _get_role_options(self):
return self._add_write_options([('view', _('Viewer')), ('edit', _('Editor')),
('none', _('None')), ('mixed', _('Mixed rights'))])
@api.model
def _get_access_via_link_mode(self):
return self._add_write_options([('mixed', _('Mixed values')),
('link_required', _('No')),
('discoverable', _('Yes'))])
@api.depends_context('uid')
@api.depends('document_ids')
def _compute_ui_values(self):
for record in self:
documents = record.document_ids
document0 = documents[:1]
record.is_single = len(documents) == 1
record.access_urls = ', '.join(d.access_url for d in documents)
owner_is_multi = any(d.owner_id != document0.owner_id for d in documents)
record.owner_id = document0.owner_id if not owner_is_multi else False
record.is_folder_only = all(d.type == 'folder' for d in documents)
record.is_readonly = any(d.user_permission != 'edit' for d in documents)
@api.depends('access_internal', 'document_ids')
def _compute_access_internal_help(self):
for record in self:
if record.access_internal.endswith('view'):
if record.is_folder_only:
record.access_internal_help = _("Can only view contents. Cannot add, modify, or delete items.")
else:
record.access_internal_help = _("Can only view. Cannot rename, move, or delete.")
elif record.access_internal.endswith('edit'):
if record.is_folder_only:
record.access_internal_help = _("Can add, modify, and delete files within this folder.")
else:
record.access_internal_help = _("Can modify, delete, and rename.")
elif record.access_internal == 'mixed':
record.access_internal_help = _('Keep the values as is (multiple values)')
else: # None
record.access_internal_help = _('Only people with access can open with the link')
@api.depends('access_via_link', 'document_ids')
def _compute_access_via_link_help(self):
for record in self:
if record.access_via_link.endswith('view'):
if record.is_folder_only:
record.access_via_link_help = _("Can only view contents. Cannot add, modify, or delete items.")
else:
record.access_via_link_help = _("Can only view. Cannot rename, move, or delete.")
elif record.access_via_link.endswith('edit'):
if record.is_folder_only:
record.access_via_link_help = _("Can add, modify, and delete files within this folder.")
else:
record.access_via_link_help = _("Can modify, delete, and rename.")
elif record.access_via_link == 'mixed':
record.access_via_link_help = _('Keep the values as is (multiple values)')
else: # None
record.access_via_link_help = _('No one on the internet can access')
@api.depends('access_internal', 'access_via_link', 'access_via_link_mode',
'share_access_ids', 'share_access_ids.role',
'share_access_ids.original_expiration_date', 'share_access_ids.expiration_date',
'share_access_ids.is_deleted')
def _compute_is_access_modified(self):
for record in self:
record.is_access_modified = bool(record._get_update_rights_params())
@api.depends('access_internal', 'access_via_link', 'share_access_ids.role', 'invite_partner_ids')
def _compute_has_warning_link_with_more_rights(self):
for record in self:
record.has_warning_link_with_more_rights = (
not record.invite_partner_ids and record.access_via_link.endswith('edit') and (
record.access_internal.endswith('view') or any(a.role.endswith('view') and not a.is_deleted for a in record.share_access_ids)))
@api.depends('access_via_link', 'invite_partner_ids', 'share_access_ids.partner_id.user_ids')
def _compute_has_warning_partners_without_access(self):
for record in self:
record.has_warning_partners_without_access = any(r.has_warning_no_access for r in record.share_access_ids)
def action_update_rights(self):
self.ensure_one()
self.document_ids.action_update_access_rights(
**self._get_update_rights_params(), no_propagation=not self.is_folder_only)
return self.action_open(self.document_ids.ids)
def action_invite_members(self):
self.ensure_one()
if not self.invite_partner_ids:
params = {
'title': _('No partners'),
'message': '',
'type': 'warning',
}
elif self.invite_partner_ids.filtered(lambda p: not p.email):
params = {
'title': _('Some emails are missing'),
'message': _('Please fill in the missing email addresses.'),
'type': 'warning',
}
else:
self.document_ids.action_update_access_rights(
partners={
partner: (self.invite_role, False)
for partner in self.invite_partner_ids
},
no_propagation=not self.is_folder_only,
)
if self.invite_notify and (
share_template := self.env.ref('documents.mail_template_document_share', raise_if_not_found=False)):
access_urls_by_partner = {}
for partner in self.invite_partner_ids:
access_urls = {}
for document in self.document_ids:
access_url = document.access_url
member = document.access_ids.filtered(lambda access:
access.partner_id == partner)
if member and member._is_signup_available():
access_url = f'{access_url}?member_signup_token={member._get_member_signup_token()}&member_id={member.id}'
access_urls[document] = access_url
access_urls_by_partner[partner] = access_urls
share_template.with_context(
documents=self.document_ids,
access_urls_by_partner=access_urls_by_partner,
message=self.invite_notify_message or "").send_mail_batch(self.invite_partner_ids.ids)
params = {
'title': _('Successfully Shared'),
'message': (
_('%s members added.', len(self.invite_partner_ids))
if len(self.invite_partner_ids) > 1
else _('Member added.')
),
'type': 'success',
'next': self.action_open(self.document_ids.ids),
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': params,
}
def action_allow_link_access(self):
if self.has_warning_partners_without_access:
self.access_via_link = f'{self.WRITE_VALUE_PREFIX}view'
self.access_via_link_mode = f'{self.WRITE_VALUE_PREFIX}link_required'
return self.action_update_rights()
@api.model
def action_open(self, document_ids):
"""Open documents sharing wizard on one or more documents."""
if not document_ids:
raise ValueError("Expected one or more documents.")
documents = self.env['documents.document'].browse(document_ids).mapped(
lambda d: d.shortcut_document_id if d.shortcut_document_id else d)
document0 = documents[0]
is_single_doc = len(documents) == 1
all_partners = self._filtered_relevant_access(documents).partner_id
access_by_partner_by_doc = {
doc: {
access.partner_id: access.role
for access in self._filtered_relevant_access(doc)
} | ({doc.owner_id.partner_id: 'edit'} if doc.owner_id else {})
for doc in documents
}
if is_single_doc: # expiration only supported for single document
expiration_per_partner = {
access.partner_id: access.expiration_date
for access in self._filtered_relevant_access(documents)
}
access_shares = []
if any(d.owner_id != document0.owner_id for d in documents): # there are no single owner of all the documents
# Mixed owner -> add owner so that their rights appear "mixed" if not editor of the documents they don't own
# Otherwise if there are only one owner, it will be displayed as such
all_partners |= documents.owner_id.partner_id
for partner in all_partners:
role0 = access_by_partner_by_doc.get(documents[0], {}).get(partner)
is_mixed = any(role0 != access_by_partner_by_doc.get(doc, {}).get(partner) for doc in documents[1:])
expiration_date = expiration_per_partner.get(partner) if is_single_doc else False
access_shares.append(Command.create(
{
'expiration_date': expiration_date,
'original_expiration_date': expiration_date,
'partner_id': partner.id,
'role': 'mixed' if is_mixed else role0,
}))
values = {
field: document0[field] if len(set(documents.mapped(field))) == 1 else 'mixed'
for field in ('access_internal', 'access_via_link')
}
if len(set(documents.mapped('is_access_via_link_hidden'))) != 1:
values['access_via_link_mode'] = 'mixed'
elif document0.is_access_via_link_hidden:
values['access_via_link_mode'] = 'link_required'
else:
values['access_via_link_mode'] = 'discoverable'
doc_sharing = self.env['documents.sharing'].create([{
'document_ids': documents.ids,
'share_access_ids': access_shares,
**values,
}])
if len(documents) == 1:
name = _("Share: %(documentName)s", documentName=documents.name)
else:
name = _("Share: %(numberOfDocuments)s files", numberOfDocuments=len(documents))
return {
'context': {
'dialog_size': 'medium',
},
'name': name,
'res_id': doc_sharing.id,
'res_model': 'documents.sharing',
'target': 'new',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'views': [[False, 'form']],
}
@api.model
def _filtered_relevant_access(self, documents):
"""Filter out expired access and non access of the documents (logs: role == False, owner access)"""
return documents.access_ids.filtered(
lambda a: a.role and a.partner_id != a.document_id.owner_id.partner_id
and (not a.expiration_date or a.expiration_date >= fields.Datetime.now()))
def _get_update_rights_params(self):
self.ensure_one()
res = {}
removed_access = self.share_access_ids.filtered('is_deleted')
# Modification of the expiration date is not supported when multiple document are selected
# In that case, expiration_date and original_expiration_date will be false and not modifiable in the UI
# If a role is changed when multiple documents are selected, we keep the expiration (using None).
modified_access = self.share_access_ids.filtered(
lambda a: (a.role.startswith(self.WRITE_VALUE_PREFIX) or a.expiration_date != a.original_expiration_date)
and not a.is_deleted)
partners = {
access.partner_id: (access.role.removeprefix(self.WRITE_VALUE_PREFIX),
access.expiration_date if self.is_single else None)
for access in modified_access
}
partners.update({
access.partner_id: (False, False)
for access in removed_access
})
if partners:
res['partners'] = partners
if self.access_internal.startswith(self.WRITE_VALUE_PREFIX):
res['access_internal'] = self.access_internal.removeprefix(self.WRITE_VALUE_PREFIX)
if self.access_via_link.startswith(self.WRITE_VALUE_PREFIX):
res['access_via_link'] = self.access_via_link.removeprefix(self.WRITE_VALUE_PREFIX)
if self.access_via_link_mode.startswith(self.WRITE_VALUE_PREFIX):
res['is_access_via_link_hidden'] = self.access_via_link_mode == f'{self.WRITE_VALUE_PREFIX}link_required'
return res