mirror of
https://github.com/OCA/knowledge.git
synced 2025-07-13 15:34:49 -06:00
Merge pull request #137 from hbrunn/8.0-document_wopi
[ADD] document_wopi
This commit is contained in:
commit
afe5a85222
52
attachment_action/README.rst
Normal file
52
attachment_action/README.rst
Normal file
@ -0,0 +1,52 @@
|
||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
======================
|
||||
Actions on attachments
|
||||
======================
|
||||
|
||||
This module was written to provide a container for other modules to add buttons for actions on attachments.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
This module does nothing useful on its own.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/knowledge/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Images
|
||||
------
|
||||
|
||||
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Holger Brunn <hbrunn@therp.nl>
|
||||
|
||||
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues.
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
3
attachment_action/__init__.py
Normal file
3
attachment_action/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
22
attachment_action/__openerp__.py
Normal file
22
attachment_action/__openerp__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Actions on attachments",
|
||||
"version": "8.0.1.0.0",
|
||||
"author": "Therp BV,Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"category": "Hidden/Dependency",
|
||||
"summary": "Enable/disable actions on attachments",
|
||||
"depends": [
|
||||
'web',
|
||||
'knowledge',
|
||||
'document',
|
||||
],
|
||||
"data": [
|
||||
'views/templates.xml',
|
||||
],
|
||||
"qweb": [
|
||||
'static/src/xml/attachment_action.xml',
|
||||
],
|
||||
}
|
BIN
attachment_action/static/description/icon.png
Normal file
BIN
attachment_action/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
37
attachment_action/static/src/css/attachment_action.css
Normal file
37
attachment_action/static/src/css/attachment_action.css
Normal file
@ -0,0 +1,37 @@
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li span.oe_sidebar_attachment_action .oe_sidebar_delete_item
|
||||
{
|
||||
position: static;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li span.oe_sidebar_attachment_action
|
||||
{
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
text-align: right;
|
||||
padding: 0px 6px;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li span.oe_sidebar_attachment_action > *
|
||||
{
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
color: #8786b7;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 1px white;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li span.oe_sidebar_attachment_action > *:hover
|
||||
{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background: #8786b7;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
||||
-moz-border-radius: 2px;
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li:hover span.oe_sidebar_attachment_action
|
||||
{
|
||||
display: block;
|
||||
}
|
32
attachment_action/static/src/js/attachment_action.js
Normal file
32
attachment_action/static/src/js/attachment_action.js
Normal file
@ -0,0 +1,32 @@
|
||||
//-*- coding: utf-8 -*-
|
||||
//Copyright 2018 Therp BV <https://therp.nl>
|
||||
//License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
openerp.attachment_action = function(instance)
|
||||
{
|
||||
instance.web.Sidebar.include({
|
||||
_extra_sidebar_attachment_fields: [],
|
||||
on_attachments_loaded: function(attachments)
|
||||
{
|
||||
var self = this, _super = this._super;
|
||||
return new instance.web.Model('ir.attachment')
|
||||
.query(this._extra_sidebar_attachment_fields)
|
||||
.filter([['id', 'in', _.pluck(attachments, 'id')]])
|
||||
.all()
|
||||
.then(function(extra_data)
|
||||
{
|
||||
_.each(attachments, function(attachment)
|
||||
{
|
||||
_.extend(attachment, _.find(
|
||||
extra_data, function(x)
|
||||
{
|
||||
return x.id == attachment.id
|
||||
}
|
||||
));
|
||||
});
|
||||
_super.apply(self, [attachments]);
|
||||
return attachments;
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
11
attachment_action/static/src/xml/attachment_action.xml
Normal file
11
attachment_action/static/src/xml/attachment_action.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-extend="Sidebar">
|
||||
<t t-jquery="a.oe_sidebar_delete_item" t-operation="before">
|
||||
<span class="oe_sidebar_attachment_action" />
|
||||
</t>
|
||||
<t t-jquery="a.oe_sidebar_delete_item">
|
||||
jQuery(this).appendTo(this.siblings('.oe_sidebar_attachment_action'));
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
11
attachment_action/views/templates.xml
Normal file
11
attachment_action/views/templates.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<template id="assets_backend" name="attachment_action assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/attachment_action/static/src/js/attachment_action.js"></script>
|
||||
<link rel="stylesheet" href="/attachment_action/static/src/css/attachment_action.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
@ -9,7 +9,7 @@
|
||||
"category": "Knowledge Management",
|
||||
"summary": "Edit attachments after upload",
|
||||
"depends": [
|
||||
'web',
|
||||
'attachment_action',
|
||||
],
|
||||
"data": [
|
||||
"views/ir_attachment.xml",
|
||||
|
@ -1,22 +0,0 @@
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li .oe-sidebar-attachment-edit
|
||||
{
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
top: 0px;
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li:hover .oe-sidebar-attachment-edit
|
||||
{
|
||||
display: inherit;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li .oe-sidebar-attachment-edit:hover
|
||||
{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background: #8786b7;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
||||
-moz-border-radius: 2px;
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
@ -25,10 +25,13 @@ openerp.attachment_edit = function(instance)
|
||||
{
|
||||
on_attachments_loaded: function(attachments)
|
||||
{
|
||||
var result = this._super.apply(this, arguments);
|
||||
this.$el.find('.oe-sidebar-attachment-edit')
|
||||
.click(this.on_attachment_edit);
|
||||
return result;
|
||||
var self = this;
|
||||
return jQuery.when(this._super.apply(this, arguments))
|
||||
.then(function()
|
||||
{
|
||||
self.$el.find('.oe-sidebar-attachment-edit')
|
||||
.click(self.on_attachment_edit);
|
||||
});
|
||||
},
|
||||
on_attachment_edit: function(e)
|
||||
{
|
||||
|
@ -4,7 +4,6 @@
|
||||
<template id="assets_backend" name="attachment_edit assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/attachment_edit/static/src/js/attachment_edit.js"></script>
|
||||
<link rel="stylesheet" href="/attachment_edit/static/src/css/attachment_edit.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
|
72
attachment_lock/README.rst
Normal file
72
attachment_lock/README.rst
Normal file
@ -0,0 +1,72 @@
|
||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
==================
|
||||
Attachment locking
|
||||
==================
|
||||
|
||||
This module was written to allow users to lock attachments for external editing.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
#. add users to the group `Attachment Locking`
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
#. go to some document with an attachment
|
||||
#. click the lock or unlock button
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/118/8.0
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* support database locks
|
||||
* support some kind of push api on locks
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/knowledge/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Images
|
||||
------
|
||||
|
||||
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Holger Brunn <hbrunn@therp.nl>
|
||||
|
||||
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues.
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
4
attachment_lock/__init__.py
Normal file
4
attachment_lock/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from . import models
|
25
attachment_lock/__openerp__.py
Normal file
25
attachment_lock/__openerp__.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Attachment locking",
|
||||
"version": "8.0.1.0.0",
|
||||
"author": "Therp BV,Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"category": "Knowledge Management",
|
||||
"summary": "Support for locks on attachments for external applications",
|
||||
"depends": [
|
||||
'attachment_action',
|
||||
],
|
||||
"data": [
|
||||
"security/res_groups.xml",
|
||||
"security/ir_rule.xml",
|
||||
'security/ir.model.access.csv',
|
||||
"views/ir_attachment_lock.xml",
|
||||
"data/ir_cron.xml",
|
||||
'views/templates.xml',
|
||||
],
|
||||
"qweb": [
|
||||
'static/src/xml/attachment_lock.xml',
|
||||
],
|
||||
}
|
13
attachment_lock/data/ir_cron.xml
Normal file
13
attachment_lock/data/ir_cron.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="cron_cleanup" model="ir.cron">
|
||||
<field name="name">Cleanup attachment locks</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="model">ir.attachment.lock</field>
|
||||
<field name="function">_cleanup_cron</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
5
attachment_lock/models/__init__.py
Normal file
5
attachment_lock/models/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from . import ir_attachment_lock
|
||||
from . import ir_attachment
|
59
attachment_lock/models/ir_attachment.py
Normal file
59
attachment_lock/models/ir_attachment.py
Normal file
@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from openerp import _, api, fields, models
|
||||
from openerp.exceptions import AccessError, ValidationError
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
lock_ids = fields.One2many(
|
||||
'ir.attachment.lock', 'attachment_id', 'Locks',
|
||||
)
|
||||
locked = fields.Boolean(compute='_compute_locked')
|
||||
can_lock = fields.Boolean(compute='_compute_locked')
|
||||
|
||||
@api.multi
|
||||
@api.depends(
|
||||
'lock_ids.create_uid', 'lock_ids.shared_user_ids',
|
||||
'lock_ids.lock_type',
|
||||
)
|
||||
def _compute_locked(self):
|
||||
for this in self:
|
||||
this.locked = bool(this.lock_ids)
|
||||
this.can_lock = bool(
|
||||
this.mapped('lock_ids.create_uid') & this.env.user or
|
||||
'shared' in this.mapped('lock_ids.lock_type')
|
||||
) or not this.locked and self.check_access_rights('write', False)
|
||||
|
||||
@api.constrains('datas', 'datas_fname')
|
||||
def _constrain_datas(self):
|
||||
for this in self:
|
||||
if not this.lock_ids:
|
||||
continue
|
||||
if not this.can_lock:
|
||||
raise ValidationError(_('Attachment is locked'))
|
||||
|
||||
@api.multi
|
||||
def lock(self, lock_type='exclusive', valid_until=None, data=None):
|
||||
data = data or {}
|
||||
if valid_until:
|
||||
data['valid_until'] = valid_until
|
||||
data['lock_type'] = lock_type
|
||||
for this in self:
|
||||
if not this.can_lock:
|
||||
raise AccessError(_('Unable to obtain lock'))
|
||||
if this.lock_ids:
|
||||
this.lock_ids.filtered(
|
||||
lambda x: x.lock_type == lock_type
|
||||
).sudo().write(dict(data, shared_user_ids=[(4, self.env.uid)]))
|
||||
else:
|
||||
this.write({'lock_ids': [(0, 0, data)]})
|
||||
|
||||
@api.multi
|
||||
def unlock(self):
|
||||
return self.mapped('lock_ids').filtered(
|
||||
lambda x: x.create_uid & self.env.user or
|
||||
x.lock_type == 'shared' and x.shared_user_ids & self.env.user
|
||||
).unlink()
|
40
attachment_lock/models/ir_attachment_lock.py
Normal file
40
attachment_lock/models/ir_attachment_lock.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from datetime import datetime, timedelta
|
||||
from openerp import api, fields, models
|
||||
|
||||
|
||||
class IrAttachmentLock(models.Model):
|
||||
_name = 'ir.attachment.lock'
|
||||
_description = 'Attachment lock'
|
||||
|
||||
create_uid = fields.Many2one(
|
||||
'res.users', 'User', required=True, default=lambda self: self.env.user,
|
||||
)
|
||||
attachment_id = fields.Many2one(
|
||||
'ir.attachment', 'Attachment', required=True, index=True,
|
||||
)
|
||||
lock_type = fields.Selection(
|
||||
[('exclusive', 'Exclusive'), ('shared', 'Shared')],
|
||||
string='Type', required=True, default='shared',
|
||||
)
|
||||
application = fields.Selection(
|
||||
[('manual', 'Manual lock')], required=True, default='manual',
|
||||
)
|
||||
valid_until = fields.Datetime(
|
||||
required=True, default=fields.Datetime.to_string(
|
||||
datetime.now() + timedelta(hours=1),
|
||||
),
|
||||
)
|
||||
shared_user_ids = fields.Many2many('res.users')
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_attachment_id', 'unique(attachment_id)', 'Lock exists!'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _cleanup_cron(self):
|
||||
self.search([
|
||||
('valid_until', '<', fields.Datetime.to_string(datetime.now())),
|
||||
]).unlink()
|
3
attachment_lock/security/ir.model.access.csv
Normal file
3
attachment_lock/security/ir.model.access.csv
Normal file
@ -0,0 +1,3 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
access_ir_attachment_lock_all,access_ir_attachment_lock,model_ir_attachment_lock,,1,0,0,0
|
||||
access_ir_attachment_lock,access_ir_attachment_lock,model_ir_attachment_lock,group_attachment_lock,1,1,1,1
|
|
17
attachment_lock/security/ir_rule.xml
Normal file
17
attachment_lock/security/ir_rule.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="rule_attachment_lock" model="ir.rule">
|
||||
<field name="name">Attachment locks</field>
|
||||
<field name="model_id" ref="model_ir_attachment_lock" />
|
||||
<field name="groups" eval="[(4, ref('group_attachment_lock'))]" />
|
||||
<field name="domain_force">
|
||||
['|', ('create_uid', '=', user.id), '&', ('lock_type', '=', 'shared'), ('shared_user_ids', 'in', user.ids)]
|
||||
</field>
|
||||
<field name="perm_read" eval="False" />
|
||||
<field name="perm_write" eval="True" />
|
||||
<field name="perm_create" eval="True" />
|
||||
<field name="perm_unlink" eval="True" />
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
10
attachment_lock/security/res_groups.xml
Normal file
10
attachment_lock/security/res_groups.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="group_attachment_lock" model="res.groups">
|
||||
<field name="name">Attachment locking</field>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]" />
|
||||
<field name="category_id" ref="base.module_category_hidden" />
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
BIN
attachment_lock/static/description/icon.png
Normal file
BIN
attachment_lock/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
0
attachment_lock/static/src/css/attachment_lock.css
Normal file
0
attachment_lock/static/src/css/attachment_lock.css
Normal file
78
attachment_lock/static/src/js/attachment_lock.js
Normal file
78
attachment_lock/static/src/js/attachment_lock.js
Normal file
@ -0,0 +1,78 @@
|
||||
//-*- coding: utf-8 -*-
|
||||
//Copyright 2018 Therp BV <https://therp.nl>
|
||||
//License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
openerp.attachment_lock = function(instance)
|
||||
{
|
||||
instance.web.Sidebar.include({
|
||||
init: function()
|
||||
{
|
||||
this._super.apply(this, arguments);
|
||||
this._extra_sidebar_attachment_fields.push('locked');
|
||||
this._extra_sidebar_attachment_fields.push('can_lock');
|
||||
},
|
||||
on_attachments_loaded: function()
|
||||
{
|
||||
var self = this;
|
||||
return jQuery.when(this._super.apply(this, arguments))
|
||||
.then(function()
|
||||
{
|
||||
self.$('.oe_sidebar_attachment_lock').click(
|
||||
self.on_attachment_lock
|
||||
);
|
||||
self.$('.oe_sidebar_attachment_locked').click(
|
||||
self.on_attachment_locked
|
||||
);
|
||||
self.$('.oe_sidebar_attachment_unlock').click(
|
||||
self.on_attachment_unlock
|
||||
);
|
||||
});
|
||||
},
|
||||
on_attachment_lock: function(e)
|
||||
{
|
||||
var self = this;
|
||||
e.stopPropagation();
|
||||
return new instance.web.Model('ir.attachment')
|
||||
.call('lock', [[jQuery(e.currentTarget).data('id')]])
|
||||
.then(function()
|
||||
{
|
||||
return self.do_attachement_update(self.dataset, self.model_id);
|
||||
});
|
||||
},
|
||||
on_attachment_locked: function(e)
|
||||
{
|
||||
var self = this;
|
||||
e.stopPropagation();
|
||||
return new instance.web.Model('ir.attachment.lock')
|
||||
.query(['create_uid', 'valid_until'])
|
||||
.filter([['attachment_id', '=', jQuery(e.currentTarget).data('id')]])
|
||||
.first()
|
||||
.then(function(lock)
|
||||
{
|
||||
new instance.web.Dialog(
|
||||
this, {
|
||||
title: instance.web._t('Locked'),
|
||||
},
|
||||
$('<div/>').text(
|
||||
_.str.sprintf(
|
||||
instance.web._t('By %s until %s'),
|
||||
lock.create_uid[1],
|
||||
instance.web.format_value(lock.valid_until, {type: 'datetime'})
|
||||
)
|
||||
)
|
||||
).open();
|
||||
});
|
||||
},
|
||||
on_attachment_unlock: function(e)
|
||||
{
|
||||
var self = this;
|
||||
e.stopPropagation();
|
||||
return new instance.web.Model('ir.attachment')
|
||||
.call('unlock', [[jQuery(e.currentTarget).data('id')]])
|
||||
.then(function()
|
||||
{
|
||||
return self.do_attachement_update(self.dataset, self.model_id);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
16
attachment_lock/static/src/xml/attachment_lock.xml
Normal file
16
attachment_lock/static/src/xml/attachment_lock.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-extend="Sidebar">
|
||||
<t t-jquery="a.oe_sidebar_delete_item" t-operation="before">
|
||||
<a t-if="section.name == 'files' and !item.callback and item.url and !item.locked and item.can_lock" class="oe_sidebar_attachment_lock" t-att-data-id="item.id" t-attf-title="Lock #{item.name}">
|
||||
<i class="fa fa-lock"></i>
|
||||
</a>
|
||||
<a t-if="section.name == 'files' and !item.callback and item.url and item.locked and item.can_lock" class="oe_sidebar_attachment_unlock" t-att-data-id="item.id" t-attf-title="Unlock #{item.name}">
|
||||
<i class="fa fa-unlock"></i>
|
||||
</a>
|
||||
<a t-if="section.name == 'files' and !item.callback and item.url and item.locked and !item.can_lock" class="oe_sidebar_attachment_locked" t-att-data-id="item.id" t-attf-title="#{item.name} is locked">
|
||||
<i class="fa fa-ban"></i>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
4
attachment_lock/tests/__init__.py
Normal file
4
attachment_lock/tests/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from . import test_attachment_lock
|
37
attachment_lock/tests/test_attachment_lock.py
Normal file
37
attachment_lock/tests/test_attachment_lock.py
Normal file
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 Therp BV <https://therp.nl>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
from base64 import b64encode
|
||||
from openerp.tests.common import TransactionCase
|
||||
from openerp.exceptions import AccessError, ValidationError
|
||||
|
||||
|
||||
class TestAttachmentLock(TransactionCase):
|
||||
def test_attachment_lock(self):
|
||||
demo = self.env.ref('base.user_demo')
|
||||
testattachment = self.env['ir.attachment'].create({
|
||||
'name': 'testattachment',
|
||||
'datas': b64encode('hello world'),
|
||||
'datas_fname': 'test.txt',
|
||||
})
|
||||
self.assertTrue(testattachment.can_lock)
|
||||
self.assertFalse(testattachment.locked)
|
||||
testattachment.lock()
|
||||
self.assertTrue(testattachment.can_lock)
|
||||
self.assertTrue(testattachment.locked)
|
||||
with self.assertRaises(ValidationError):
|
||||
testattachment.sudo(demo).write({
|
||||
'datas': b64encode('hello world2'),
|
||||
})
|
||||
with self.assertRaises(AccessError):
|
||||
testattachment.sudo(demo).lock()
|
||||
demo.write({'groups_id': [
|
||||
(4, self.env.ref('attachment_lock.group_attachment_lock').id),
|
||||
]})
|
||||
with self.assertRaises(AccessError):
|
||||
testattachment.sudo(demo).lock()
|
||||
testattachment.unlock()
|
||||
self.assertTrue(testattachment.sudo(demo).can_lock)
|
||||
testattachment.sudo(demo).lock()
|
||||
self.assertTrue(testattachment.sudo(demo).can_lock)
|
||||
self.assertTrue(testattachment.sudo(demo).locked)
|
42
attachment_lock/views/ir_attachment_lock.xml
Normal file
42
attachment_lock/views/ir_attachment_lock.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="view_ir_attachment_lock_tree" model="ir.ui.view">
|
||||
<field name="model">ir.attachment.lock</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="attachment_id" />
|
||||
<field name="create_uid" />
|
||||
<field name="valid_until" />
|
||||
<field name="lock_type" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_ir_attachment_lock_form" model="ir.ui.view">
|
||||
<field name="model">ir.attachment.lock</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="attachment_id" />
|
||||
<field name="create_uid" />
|
||||
<field name="valid_until" />
|
||||
<field name="lock_type" />
|
||||
<field name="shared_user_ids" widget="many2many_tags" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<act_window
|
||||
id="action_ir_attachment_lock"
|
||||
name="Locks"
|
||||
res_model="ir.attachment.lock"
|
||||
view_mode="tree,form"
|
||||
groups="group_attachment_lock"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_ir_attachment_lock"
|
||||
parent="knowledge.menu_document_configuration"
|
||||
action="action_ir_attachment_lock"
|
||||
/>
|
||||
</data>
|
||||
</openerp>
|
11
attachment_lock/views/templates.xml
Normal file
11
attachment_lock/views/templates.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<template id="assets_backend" name="attachment_lock assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/attachment_lock/static/src/js/attachment_lock.js"></script>
|
||||
<link rel="stylesheet" href="/attachment_lock/static/src/css/attachment_lock.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
@ -27,7 +27,7 @@
|
||||
'summary': 'Preview attachments supported by Viewer.js',
|
||||
"category": "Knowledge Management",
|
||||
"depends": [
|
||||
'web',
|
||||
'attachment_action',
|
||||
],
|
||||
"data": [
|
||||
"view/attachment_preview.xml",
|
||||
|
@ -1,24 +1,3 @@
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li .oe-sidebar-attachment-preview
|
||||
{
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 0px;
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li:hover .oe-sidebar-attachment-preview
|
||||
{
|
||||
display: inherit;
|
||||
}
|
||||
.openerp .oe_sidebar .oe_dropdown_menu li .oe-sidebar-attachment-preview:hover
|
||||
{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background: #8786b7;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
||||
-moz-border-radius: 2px;
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.openerp .oe-binary-preview
|
||||
{
|
||||
cursor: pointer;
|
||||
|
@ -47,11 +47,14 @@ openerp.attachment_preview = function(instance)
|
||||
{
|
||||
on_attachments_loaded: function(attachments)
|
||||
{
|
||||
var result = this._super.apply(this, arguments);
|
||||
this.$el.find('.oe-sidebar-attachment-preview')
|
||||
.click(this.on_attachment_preview);
|
||||
this.update_preview_buttons();
|
||||
return result;
|
||||
var self = this;
|
||||
return jQuery.when(this._super.apply(this, arguments))
|
||||
.then(function()
|
||||
{
|
||||
self.$el.find('.oe-sidebar-attachment-preview')
|
||||
.click(self.on_attachment_preview);
|
||||
self.update_preview_buttons();
|
||||
});
|
||||
},
|
||||
on_attachment_preview: function(e)
|
||||
{
|
||||
|
80
document_wopi/README.rst
Normal file
80
document_wopi/README.rst
Normal file
@ -0,0 +1,80 @@
|
||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
====
|
||||
WOPI
|
||||
====
|
||||
|
||||
This module was written to implement the `WOPI protocol <http://wopi.readthedocs.io/projects/wopirest>`_ in order to allow online collaborative editing and viewing of office files. This works with `LibreOffice online <https://wiki.documentfoundation.org/Development/LibreOffice_Online>`_/`Collabora online <https://www.collaboraoffice.com/collabora-online>`_ and probably also with Office 365.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To install this module, you need to:
|
||||
|
||||
#. have a WOPI client available. For testing, use one of the `Collabora <https://hub.docker.com/r/collabora/code>`_ or `Libreoffice <https://hub.docker.com/r/libreoffice/online/>`_ docker images. The first works out of the box, but is rate limited, the second needs manual interaction, but is unmodified. The `Nextcloud documentation <https://nextcloud.com/collaboraonline/>`_ contains some information on how to get this running
|
||||
#. have your instance accessible on port 443 via your WOPI client and should have a valid SSL certificate
|
||||
#. use a dbfilter if necessary to have unauthenticated requests ending up in the database you want
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
#. fill in your WOPI client URL in the configuration wizard
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
#. on any supported attachment, click the edit button
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/118/8.0
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* implement quasi permanent public shares a la google docs
|
||||
* whoever want to get their hands dirty with Microsoft should find our how to integrate this with Office 365
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/knowledge/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Images
|
||||
------
|
||||
|
||||
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Holger Brunn <hbrunn@therp.nl>
|
||||
|
||||
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues.
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
5
document_wopi/__init__.py
Normal file
5
document_wopi/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import models
|
||||
from . import controllers
|
32
document_wopi/__openerp__.py
Normal file
32
document_wopi/__openerp__.py
Normal file
@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "WOPI",
|
||||
"version": "8.0.1.0.0",
|
||||
"author": "Therp BV,Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"category": "Knowledge Management",
|
||||
"summary": "Allow online collaborative editing of your documents",
|
||||
"depends": [
|
||||
'knowledge',
|
||||
'attachment_action',
|
||||
'attachment_lock',
|
||||
],
|
||||
"demo": [
|
||||
"demo/ir_config_parameter.xml",
|
||||
],
|
||||
"data": [
|
||||
"security/ir_rule.xml",
|
||||
"data/ir_actions_todo.xml",
|
||||
"views/knowledge_config_settings.xml",
|
||||
'views/templates.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
"qweb": [
|
||||
"static/src/xml/document_wopi.xml",
|
||||
],
|
||||
"images": [
|
||||
"images/screenshot.png",
|
||||
],
|
||||
}
|
4
document_wopi/controllers/__init__.py
Normal file
4
document_wopi/controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import main
|
102
document_wopi/controllers/main.py
Normal file
102
document_wopi/controllers/main.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from openerp import http, fields
|
||||
from openerp.http import request
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WopiEndpoints(http.Controller):
|
||||
@http.route(
|
||||
'/wopi/files/<model("ir.attachment"):attachment>', type='http',
|
||||
auth='public',
|
||||
)
|
||||
def files(self, attachment, access_token, **kwargs):
|
||||
token = self._verify(access_token)
|
||||
if not token:
|
||||
_logger.error(request.httprequest.headers)
|
||||
return request.not_found()
|
||||
attachment = token.attachment_id
|
||||
_logger.debug(request.httprequest.headers)
|
||||
return json.dumps({
|
||||
'BaseFileName': attachment.name,
|
||||
'OwnerId': attachment.create_uid.id,
|
||||
'Size': attachment.file_size,
|
||||
'UserId': token.user_id.id,
|
||||
'Version': str(self._timestamp(attachment)),
|
||||
'UserFriendlyName': token.user_id.display_name,
|
||||
'UserCanWrite': attachment.check_access_rights('write', False),
|
||||
'SupportsUpdate': True,
|
||||
'SupportsGetLock': True,
|
||||
'SupportsLocks': True,
|
||||
'LastModifiedTime': fields.Datetime.from_string(
|
||||
attachment.write_date
|
||||
).isoformat(),
|
||||
'UserCanNotWriteRelative': True,
|
||||
'DisablePrint': False,
|
||||
'DisableExport': False,
|
||||
'DisableCopy': False,
|
||||
'DisableInactiveMessages': False,
|
||||
'HidePrintOption': False,
|
||||
'HideSaveOption': False,
|
||||
'HideExportOption': False,
|
||||
'EnableOwnerTermination': False,
|
||||
'UserExtraInfo': False,
|
||||
'WatermarkText': '',
|
||||
'PostMessageOrigin': False,
|
||||
})
|
||||
|
||||
@http.route(
|
||||
'/wopi/files/<model("ir.attachment"):attachment>/contents',
|
||||
type='http', auth='public',
|
||||
)
|
||||
def files_contents(self, attachment, access_token, **kwargs):
|
||||
token = self._verify(access_token)
|
||||
attachment = token.attachment_id
|
||||
if not token:
|
||||
_logger.error(request.httprequest.headers)
|
||||
return request.not_found()
|
||||
_logger.debug(request.httprequest.headers)
|
||||
if 'X-WOPI-Override' in request.httprequest.headers:
|
||||
name = '_files_contents_%s' % (
|
||||
request.httprequest.headers['X-WOPI-Override']
|
||||
)
|
||||
if hasattr(self, name):
|
||||
return getattr(self, name)(attachment, token, **kwargs)
|
||||
return request.not_found()
|
||||
return request.make_response(
|
||||
base64.b64decode(attachment.datas),
|
||||
[('X-WOPI-ItemVersion', str(self._timestamp(attachment)))],
|
||||
)
|
||||
|
||||
def _files_contents_PUT(self, attachment, token, **kwargs):
|
||||
if not attachment.can_lock:
|
||||
return request.not_found()
|
||||
if not attachment.locked:
|
||||
attachment.lock()
|
||||
attachment.write({'datas': base64.b64encode(request.httprequest.data)})
|
||||
return request.make_response(
|
||||
json.dumps({
|
||||
'LastModifiedTime': fields.Datetime.from_string(
|
||||
attachment.create_date
|
||||
).isoformat(),
|
||||
}),
|
||||
[
|
||||
('X-WOPI-Lock', str(attachment.lock_ids.ids)),
|
||||
('X-WOPI-ItemVersion', str(self._timestamp(attachment))),
|
||||
],
|
||||
)
|
||||
|
||||
def _verify(self, access_token):
|
||||
return request.env['document.wopi.access.token'].sudo()._verify_token(
|
||||
access_token
|
||||
)
|
||||
|
||||
def _timestamp(self, attachment):
|
||||
return int(time.mktime(
|
||||
fields.Datetime.from_string(attachment.write_date).timetuple()
|
||||
))
|
10
document_wopi/data/ir_actions_todo.xml
Normal file
10
document_wopi/data/ir_actions_todo.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="document_wopi_todo" model="ir.actions.todo">
|
||||
<field name="name">Configure document_wopi</field>
|
||||
<field name="action_id" ref="knowledge.action_knowledge_configuration" />
|
||||
<field name="type">once</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
9
document_wopi/demo/ir_config_parameter.xml
Normal file
9
document_wopi/demo/ir_config_parameter.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="param_client" model="ir.config_parameter">
|
||||
<field name="key">document_wopi.client</field>
|
||||
<field name="value">https://docs.therp.nl</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
BIN
document_wopi/images/screenshot.png
Normal file
BIN
document_wopi/images/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
6
document_wopi/models/__init__.py
Normal file
6
document_wopi/models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import knowledge_config_settings
|
||||
from . import document_wopi
|
||||
from . import document_wopi_access_token
|
47
document_wopi/models/document_wopi.py
Normal file
47
document_wopi/models/document_wopi.py
Normal file
@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
import requests
|
||||
import urlparse
|
||||
from openerp import api, models, tools
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class DocumentWopi(models.AbstractModel):
|
||||
_name = 'document.wopi'
|
||||
_description = 'Implement the WOPI protocol'
|
||||
|
||||
@api.model
|
||||
def _get_url(self, path):
|
||||
client = self.env['ir.config_parameter'].get_param(
|
||||
'document_wopi.client'
|
||||
)
|
||||
assert client, 'You need to define your WOPI client in the settings'
|
||||
return urlparse.urljoin(client, path)
|
||||
|
||||
@api.model
|
||||
def _call(self, path, query=None, headers=None):
|
||||
return requests.get(
|
||||
self._get_url(path), params=query, headers=headers
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_access_token(self, attachment_id):
|
||||
"""Return an access token to pass to the WOPI client. Make sure the
|
||||
user in question can access the resource"""
|
||||
self.env['ir.attachment'].browse(attachment_id).read([])
|
||||
return self.env['document.wopi.access.token']._get_token(
|
||||
self.env['ir.attachment'].browse(attachment_id)
|
||||
).id
|
||||
|
||||
@tools.ormcache()
|
||||
@api.model
|
||||
def _discovery(self):
|
||||
response = self.env['document.wopi']._call('hosting/discovery')
|
||||
xml = etree.fromstring(response.text)
|
||||
result = {}
|
||||
for action in xml.xpath('//action'):
|
||||
ext = action.attrib['ext']
|
||||
name = action.attrib['name']
|
||||
result[(ext, name)] = dict(action.attrib)
|
||||
return result
|
109
document_wopi/models/document_wopi_access_token.py
Normal file
109
document_wopi/models/document_wopi_access_token.py
Normal file
@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
import time
|
||||
import mimetypes
|
||||
import os.path
|
||||
import uuid
|
||||
import urllib
|
||||
import urlparse
|
||||
from datetime import datetime, timedelta
|
||||
from openerp import api, fields, models
|
||||
from openerp.exceptions import AccessDenied
|
||||
|
||||
|
||||
class DocumentWopiAccessToken(models.Model):
|
||||
_name = 'document.wopi.access.token'
|
||||
_description = 'Access token for WOPI requests'
|
||||
|
||||
user_id = fields.Many2one(
|
||||
'res.users', 'User', required=True, ondelete='cascade',
|
||||
)
|
||||
attachment_id = fields.Many2one(
|
||||
'ir.attachment', 'Attachment', required=True, ondelete='cascade',
|
||||
)
|
||||
valid_until = fields.Datetime()
|
||||
token = fields.Char(required=True)
|
||||
token_ttl = fields.Integer(compute='_compute_token_ttl')
|
||||
action_url = fields.Char(compute='_compute_action_url')
|
||||
|
||||
@api.model
|
||||
def _get_token(self, attachment):
|
||||
"""Create or return a token ID for some attachment for the current
|
||||
user. This implies creating a shared lock for the attachment"""
|
||||
result = self.search([
|
||||
('user_id', '=', self.env.user.id),
|
||||
('attachment_id', '=', attachment.id),
|
||||
'|',
|
||||
('valid_until', '=', False),
|
||||
('valid_until', '>=', fields.Datetime.now()),
|
||||
], limit=1)
|
||||
valid_until = fields.Datetime.to_string(
|
||||
datetime.now() +
|
||||
timedelta(self.env['ir.config_parameter'].get_param(
|
||||
'document_wopi.token_validity', 1800,
|
||||
))
|
||||
)
|
||||
if result:
|
||||
result.write({'valid_until': valid_until})
|
||||
else:
|
||||
result = self.create({
|
||||
'user_id': self.env.user.id,
|
||||
'attachment_id': attachment.id,
|
||||
'valid_until': valid_until,
|
||||
# TODO: is this random enough or do we need some secret
|
||||
# generator?
|
||||
'token': str(uuid.uuid4()),
|
||||
})
|
||||
result.attachment_id.lock(lock_type='shared', valid_until=valid_until)
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def _compute_token_ttl(self):
|
||||
for this in self:
|
||||
if not this.valid_until:
|
||||
continue
|
||||
this.token_ttl = int(time.mktime(
|
||||
fields.Datetime.from_string(this.valid_until).timetuple()
|
||||
))
|
||||
|
||||
@api.multi
|
||||
def _compute_action_url(self):
|
||||
wopi = self.env['document.wopi']
|
||||
discovery = wopi._discovery()
|
||||
for this in self:
|
||||
dummy, ext = os.path.splitext(this.attachment_id.datas_fname)
|
||||
if not ext:
|
||||
ext = mimetypes.guess_extension(this.attachment_id.file_type)
|
||||
if not ext:
|
||||
continue
|
||||
ext = ext[1:]
|
||||
action = discovery.get(
|
||||
(ext, 'edit'), discovery.get((ext, 'view')),
|
||||
)
|
||||
if not action:
|
||||
continue
|
||||
|
||||
this.action_url = wopi._get_url(
|
||||
action.get('urlsrc', '')
|
||||
) + '?' + urllib.urlencode({
|
||||
'WOPISrc': urlparse.urljoin(
|
||||
self.env['ir.config_parameter']
|
||||
.get_param('web.base.url'),
|
||||
'/wopi/files/%d' % this.attachment_id.id,
|
||||
)
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _verify_token(self, token):
|
||||
"""Be sure a token is valid; return a falsy value for invalid tokens"""
|
||||
access_token = self.search([('token', '=', token)])
|
||||
if not access_token:
|
||||
return self.browse([])
|
||||
try:
|
||||
access_token.attachment_id.sudo(access_token.user_id).read([])
|
||||
except AccessDenied:
|
||||
return self.browse([])
|
||||
if access_token.valid_until < fields.Datetime.now():
|
||||
return self.browse([])
|
||||
return access_token.sudo(access_token.user_id)
|
36
document_wopi/models/knowledge_config_settings.py
Normal file
36
document_wopi/models/knowledge_config_settings.py
Normal file
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from openerp import _, api, fields, models
|
||||
from openerp.exceptions import Warning as UserError
|
||||
|
||||
|
||||
class KnowledgeConfigSettings(models.TransientModel):
|
||||
_inherit = 'knowledge.config.settings'
|
||||
|
||||
document_wopi_client = fields.Char(
|
||||
'WOPI client', required=True,
|
||||
default=lambda self: self._default_document_wopi_client()
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def set_document_wopi_client(self):
|
||||
self.ensure_one()
|
||||
self.env['ir.config_parameter'].set_param(
|
||||
'document_wopi.client', self.document_wopi_client
|
||||
)
|
||||
self.env['document.wopi']._discovery.clear_cache(
|
||||
self.env['document.wopi']
|
||||
)
|
||||
response = self.env['document.wopi']._discovery()
|
||||
if not response:
|
||||
raise UserError(
|
||||
_("%s doesn't seem to support WOPI discovery") %
|
||||
self.document_wopi_client
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_document_wopi_client(self):
|
||||
return self.env['ir.config_parameter'].get_param(
|
||||
'document_wopi.client'
|
||||
)
|
2
document_wopi/security/ir.model.access.csv
Normal file
2
document_wopi/security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
access_document_wopi_access_token,access_document_wopi_access_token,model_document_wopi_access_token,base.group_document_user,1,1,1,1
|
|
10
document_wopi/security/ir_rule.xml
Normal file
10
document_wopi/security/ir_rule.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="rule_document_wopi_access_token" model="ir.rule">
|
||||
<field name="name">Users can only work on their own access tokens</field>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="model_id" ref="document_wopi.model_document_wopi_access_token" />
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
BIN
document_wopi/static/description/icon.png
Normal file
BIN
document_wopi/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
9
document_wopi/static/src/css/document_wopi.css
Normal file
9
document_wopi/static/src/css/document_wopi.css
Normal file
@ -0,0 +1,9 @@
|
||||
#document_wopi_iframe
|
||||
{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
border: none;
|
||||
}
|
89
document_wopi/static/src/js/document_wopi.js
Normal file
89
document_wopi/static/src/js/document_wopi.js
Normal file
@ -0,0 +1,89 @@
|
||||
//-*- coding: utf-8 -*-
|
||||
//© 2017 Therp BV <http://therp.nl>
|
||||
//License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
openerp.document_wopi = function(instance)
|
||||
{
|
||||
var _t = instance.web._t;
|
||||
instance.web.Sidebar.include(
|
||||
{
|
||||
on_attachments_loaded: function(attachments)
|
||||
{
|
||||
var self = this;
|
||||
return jQuery.when(this._super.apply(this, arguments))
|
||||
.then(function()
|
||||
{
|
||||
self.$el.find('.oe-sidebar-document-wopi')
|
||||
.click(self.on_document_wopi);
|
||||
});
|
||||
},
|
||||
on_document_wopi: function(e)
|
||||
{
|
||||
var self = this,
|
||||
$target = jQuery(e.currentTarget),
|
||||
attachment_id = $target.data('id');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return new instance.web.Model('document.wopi').call(
|
||||
'get_access_token', [attachment_id]
|
||||
).then(function(access_token_id)
|
||||
{
|
||||
return self.document_wopi_frame(access_token_id);
|
||||
});
|
||||
},
|
||||
document_wopi_frame: function(access_token_id)
|
||||
{
|
||||
this.do_action({
|
||||
type: 'ir.actions.client',
|
||||
name: 'document_wopi',
|
||||
tag: 'document_wopi',
|
||||
params: {
|
||||
access_token_id: access_token_id,
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
instance.document_wopi.OfficeWidget = instance.web.Widget.extend({
|
||||
'template': 'DocumentWopiOfficeWidget',
|
||||
init: function(parent, options)
|
||||
{
|
||||
this.params = options.params;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
start: function()
|
||||
{
|
||||
return this._super.apply(this, arguments)
|
||||
.then(this.proxy('get_token_values'))
|
||||
.then(this.proxy('call_wopi_client'));
|
||||
},
|
||||
get_token_values: function()
|
||||
{
|
||||
var self = this;
|
||||
return new instance.web.Model('document.wopi.access.token').query(
|
||||
['token', 'token_ttl', 'attachment_id', 'action_url']
|
||||
)
|
||||
.filter([['id', '=', this.params.access_token_id]])
|
||||
.first()
|
||||
.then(function(data)
|
||||
{
|
||||
self.params = _.extend(self.params, data);
|
||||
});
|
||||
},
|
||||
call_wopi_client: function()
|
||||
{
|
||||
if(!this.params.action_url)
|
||||
{
|
||||
throw instance.web._t('Unsupported document!');
|
||||
}
|
||||
this.$('form').attr('action', this.params.action_url);
|
||||
this.$('form input[name="access_token"]').val(this.params.token);
|
||||
this.$('form input[name="access_token_ttl"]').val(
|
||||
this.params.token_ttl
|
||||
);
|
||||
this.$('form').submit();
|
||||
},
|
||||
});
|
||||
instance.web.client_actions.add(
|
||||
'document_wopi', 'instance.document_wopi.OfficeWidget'
|
||||
);
|
||||
};
|
17
document_wopi/static/src/xml/document_wopi.xml
Normal file
17
document_wopi/static/src/xml/document_wopi.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-extend="Sidebar">
|
||||
<t t-jquery="a.oe_sidebar_delete_item" t-operation="before">
|
||||
<a t-if="section.name == 'files' and !item.callback and item.url" href="#" class="oe-sidebar-document-wopi" t-att-data-id="item.id" t-att-data-url="item.url" t-attf-title="Edit #{item.name}">
|
||||
<img t-att-src='_s + "/web/static/src/img/icons/STOCK_EDIT.png"' width="12" height="12" border="0"/>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<div t-name="DocumentWopiOfficeWidget">
|
||||
<form id="document_wopi_form" name="document_wopi_form" target="document_wopi_iframe" action="" method="post">
|
||||
<input name="access_token" value="" type="hidden"/>
|
||||
<input name="access_token_ttl" value="" type="hidden"/>
|
||||
</form>
|
||||
<iframe id="document_wopi_iframe" name="document_wopi_iframe" allowfullscreen="allowfullscreen" />
|
||||
</div>
|
||||
</templates>
|
4
document_wopi/tests/__init__.py
Normal file
4
document_wopi/tests/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import test_document_wopi
|
56
document_wopi/tests/test_document_wopi.py
Normal file
56
document_wopi/tests/test_document_wopi.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2017 Therp BV <http://therp.nl>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from base64 import b64encode
|
||||
from openerp.tests.common import TransactionCase
|
||||
from openerp.exceptions import AccessError
|
||||
from openerp.tools import mute_logger
|
||||
|
||||
|
||||
class TestDocumentWopi(TransactionCase):
|
||||
def test_document_wopi(self):
|
||||
config = self.env['knowledge.config.settings'].create({})
|
||||
self.assertEqual(
|
||||
config.document_wopi_client,
|
||||
self.env['ir.config_parameter'].get_param('document_wopi.client'),
|
||||
)
|
||||
config.set_document_wopi_client()
|
||||
discovery = self.env['document.wopi']._discovery()
|
||||
self.assertIn(('odt', 'edit'), discovery)
|
||||
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': 'testattachment',
|
||||
'type': 'binary',
|
||||
'datas': b64encode('hello world'),
|
||||
'datas_fname': 'testattachment.fodt',
|
||||
})
|
||||
|
||||
demo_user = self.env.ref('base.user_demo')
|
||||
with mute_logger('openerp.addons.base.ir.ir_model'):
|
||||
with self.assertRaises(AccessError):
|
||||
token = self.env['document.wopi.access.token'].sudo(
|
||||
demo_user
|
||||
)._get_token(attachment)
|
||||
token = self.env['document.wopi.access.token']._get_token(attachment)
|
||||
self.assertTrue(token.action_url)
|
||||
self.assertTrue(
|
||||
self.env['document.wopi.access.token']
|
||||
._verify_token(token.token)
|
||||
)
|
||||
self.assertFalse(
|
||||
self.env['document.wopi.access.token'].sudo(demo_user)
|
||||
._verify_token(token.token)
|
||||
)
|
||||
demo_user.write({
|
||||
'groups_id': [
|
||||
(4, self.env.ref('attachment_lock.group_attachment_lock').id),
|
||||
],
|
||||
})
|
||||
token = self.env['document.wopi.access.token'].sudo(
|
||||
demo_user
|
||||
)._get_token(attachment)
|
||||
self.assertTrue(token)
|
||||
self.assertTrue(
|
||||
self.env['document.wopi.access.token'].sudo(demo_user)
|
||||
._verify_token(token.token)
|
||||
)
|
16
document_wopi/views/knowledge_config_settings.xml
Normal file
16
document_wopi/views/knowledge_config_settings.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="view_knowledge_configuration" model="ir.ui.view">
|
||||
<field name="model">knowledge.config.settings</field>
|
||||
<field name="inherit_id" ref="knowledge.view_knowledge_configuration" />
|
||||
<field name="arch" type="xml">
|
||||
<group position="after">
|
||||
<group name="document_wopi">
|
||||
<field name="document_wopi_client" placeholder="https://your.wopi.client" />
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
11
document_wopi/views/templates.xml
Normal file
11
document_wopi/views/templates.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<template id="assets_backend" name="document_wopi assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/document_wopi/static/src/js/document_wopi.js"></script>
|
||||
<link rel="stylesheet" href="/document_wopi/static/src/css/document_wopi.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
Loading…
Reference in New Issue
Block a user