310 lines
15 KiB
Python
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
|