[ADD] attachment_lock

This commit is contained in:
Holger Brunn 2018-02-04 00:35:39 +01:00
parent 4f88b026c5
commit 0f5ee4b2d9
No known key found for this signature in database
GPG Key ID: 01C9760FECA3AE18
18 changed files with 436 additions and 0 deletions

View 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.

View 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

View 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',
],
}

View 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>

View 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

View 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()

View 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()

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_attachment_lock_all access_ir_attachment_lock model_ir_attachment_lock 1 0 0 0
3 access_ir_attachment_lock access_ir_attachment_lock model_ir_attachment_lock group_attachment_lock 1 1 1 1

View 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), '&amp;', ('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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View 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);
});
},
});
};

View 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>

View 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

View 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)

View 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>

View 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>