3
0
Fork 0

[ADD] web_listview_custom_column (#787)

8.0
Holger Brunn 2018-08-26 13:32:37 +02:00 committed by Pedro M. Baeza
parent d5251f7326
commit 9128286df7
15 changed files with 565 additions and 0 deletions

View File

@ -0,0 +1 @@
server-tools https://github.com/hbrunn/server-tools 8.0-base_view_inheritance_extension-user_ids

View File

@ -0,0 +1,79 @@
.. 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
==========================
Custom columns in listview
==========================
This module was written to allow users to rearrange columns in list views. This can be done organization wide or just for the user herself.
Configuration
=============
To configure this module, you need to add users supposed to be able to customize columns to the group `Customize list views`. Note that this permission allows all sorts of mischief up to privilege escalation under certain circumstances, so this is only for trusted users.
Usage
=====
To use this module, you need to:
#. go to some list and click the columns symbol left of the view switcher
#. use the buttons appearing in column headers to remove and rearrange columns
#. use the dropdown appearing next to the columns symbol to add columns
#. use the person or group symbols next to the dropdown to switch between for whom you customize
#. use the cross next to those to delete your customization. If there's a customization both for yourself and everyone, the first reset will put you on the customization for everyone, and the second will delete the customization for everyone
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/162/8.0
Known issues
============
- this addon creates standard view overrides. Those are created with priority 10000 to avoid side effects, but the views will break if you remove fields from the database. Uninstalling the module will remove all view customizations.
- when rearranging columns, invisible columns count. So if it seems like nothing happens, you'll probably have some invisible columns
Roadmap
=======
- support some kind of group level customization
- allow sharing customizations with others
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/web/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 2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Custom columns in listview",
"version": "8.0.1.0.0",
"author": "Therp BV,Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Tools",
"summary": "Remove or add columns to list views",
"depends": [
'web',
'base_view_inheritance_extension',
],
"data": [
"security/res_groups.xml",
'views/templates.xml',
'security/ir.model.access.csv',
],
"qweb": [
'static/src/xml/web_listview_custom_column.xml',
],
}

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import ir_ui_view

View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
# © 2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from lxml import etree
from openerp import _, api, models, tools
class IrUiView(models.Model):
_inherit = 'ir.ui.view'
@api.multi
def custom_column(self, diff):
"""Apply a change for a custom view. Create a new custom view or
view override as necessary"""
self.ensure_one()
tree = etree.fromstring(self.read_combined(self.id)['arch'])
customized_view = self.env.ref(
self._custom_column_xmlid(diff), raise_if_not_found=False
) or self.browse([])
if diff['operation'] == 'add':
etree.SubElement(tree, 'field', {'name': diff['name']})
elif diff['operation'] == 'remove':
for element in tree:
if element.attrib['name'] == diff['name'] and\
element.tag == 'field':
tree.remove(element)
elif diff['operation'] == 'left':
for element in tree:
if element.attrib['name'] == diff['name'] and\
element.tag == 'field' and\
element.getprevious() is not None:
element.getprevious().addprevious(element)
break
elif diff['operation'] == 'right':
for element in tree:
if element.attrib['name'] == diff['name'] and\
element.tag == 'field' and\
element.getnext() is not None:
element.getnext().addnext(element)
break
elif diff['operation'] == 'reset':
customized_view.unlink()
return []
elif diff['operation'] == 'to_user':
diff['type'] = 'user'
customized_view = self.env.ref(
self._custom_column_xmlid(diff), raise_if_not_found=False
) or self.browse([])
elif diff['operation'] == 'to_all':
customized_view.unlink()
diff['type'] = 'all'
customized_view = self.env.ref(
self._custom_column_xmlid(diff), raise_if_not_found=False
) or self.browse([])
else:
raise NotImplementedError(
'Unknown operation %s' % diff['operation']
)
replacement = etree.Element('tree', {'position': 'replace'})
replacement.append(tree)
arch = etree.tostring(replacement, pretty_print=True)
if customized_view:
customized_view.write({'arch': arch})
else:
customized_view = self._custom_column_create_view(diff, arch)
return customized_view.id
@api.multi
def custom_column_desc(self):
"""Return metadata necessary for UI"""
self.ensure_one()
return {
'fields': self.env[self.model].fields_get(),
'type': bool(self.env.ref(
self._custom_column_xmlid({'type': 'user'}),
raise_if_not_found=False
)) and 'user' or bool(self.env.ref(
self._custom_column_xmlid({'type': 'all'}),
raise_if_not_found=False
)) and 'all' or 'user',
}
@api.multi
def _custom_column_xmlid(self, diff, qualify=True):
"""Return an xmlid for the view of a type of customization"""
self.ensure_one()
customization_type = diff['type']
return '%scustom_view_%d_%s%s' % (
qualify and 'web_listview_custom_column.' or '',
self.id,
customization_type,
'_%d' % self.env.uid if customization_type == 'user' else '',
)
@api.multi
def _custom_column_create_view(self, diff, arch):
"""Actually create a view for customization"""
self.ensure_one()
values = self.copy_data(default={
'name': _('%s customized') % self.name,
'arch': arch,
'inherit_id': self.id,
'mode': 'extension',
'priority': 10000 + (diff['type'] == 'user' and 1 or 0),
'user_ids': [(4, self.env.uid)] if diff['type'] == 'user' else [],
})[0]
result = self.create(values)
self.env['ir.model.data'].create({
'name': self._custom_column_xmlid(diff, qualify=False),
'module': 'web_listview_custom_column',
'model': self._name,
'res_id': result.id,
'noupdate': True,
})
return result
@api.multi
def _check_xml(self):
"""Don't validate our custom views, this will break in init mode"""
if self.env.registry._init:
self = self.filtered(
lambda x: not x.xml_id or not x.xml_id.startswith(
'web_listview_custom_column.custom_view_'
)
)
return super(IrUiView, self)._check_xml()
_constraints = [(_check_xml, 'Invalid view definition', ['arch'])]
@api.model
def get_inheriting_views_arch(self, view_id, model):
"""Don't apply our view inheritance in init mode for the same reason"""
return [
(arch, view_id_)
for arch, view_id_ in
super(IrUiView, self).get_inheriting_views_arch(view_id, model)
if (not self.env.registry._init or tools.config['test_enable']) or
not self.sudo().browse(view_id_).xml_id or
not self.sudo().browse(view_id_).xml_id.startswith(
'web_listview_custom_column.custom_view_'
)
]

View File

@ -0,0 +1,2 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
crud_ir_ui_view,"CRUD ir.ui.view",base.model_ir_ui_view,group_custom_column,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 crud_ir_ui_view CRUD ir.ui.view base.model_ir_ui_view group_custom_column 1 1 1 1

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data noupdate="1">
<record id="group_custom_column" model="res.groups">
<field name="name">Customize list views</field>
<field name="category_id" ref="base.module_category_hidden" />
</record>
<record id="base.group_system" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_custom_column'))]" />
</record>
</data>
</openerp>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,40 @@
.openerp .oe_view_manager_custom_column
{
height: 24px;
line-height: 24px;
display: inline-block;
border: 1px solid #ababab;
cursor: pointer;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
max-width: calc(100% - 200px);
}
.openerp .oe_view_manager_custom_column a
{
padding: 1px 6px;
color: #4c4c4c;
}
.openerp .oe_view_manager_custom_column a.active i
{
text-shadow: 0 0 3px #ababab;
}
.openerp .oe_view_manager_custom_column select
{
margin: 1px;
background: transparent;
max-width: calc(100% - 110px);
}
.openerp .oe_list th .oe_custom_column_remove,
.openerp .oe_list th .oe_custom_column_left,
.openerp .oe_list th .oe_custom_column_right
{
color: #4c4c4c;
font-size: smaller;
}
.openerp .oe_list th .oe_custom_column_remove:hover,
.openerp .oe_list th .oe_custom_column_left:hover,
.openerp .oe_list th .oe_custom_column_right:hover
{
text-shadow: 0px 0px 1px;
}

View File

@ -0,0 +1,136 @@
//-*- coding: utf-8 -*-
//Copyright 2017 Therp BV <http://therp.nl>
//License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
openerp.web_listview_custom_column = function(instance)
{
instance.web.ListView.include({
init: function(parent, dataset, view_id, options)
{
this._super.apply(this, arguments)
this.ViewManager.on('switch_mode', this, function(view_type)
{
this._custom_column_get_element().toggle(view_type == 'list');
});
},
load_list: function()
{
var self = this;
this._super.apply(this, arguments);
this.$custom_column = jQuery(instance.web.qweb.render(
'ListView.CustomColumn', {widget: this}
));
this._custom_column_get_element()
.empty()
.append(this.$custom_column);
this.$custom_column.filter('.oe_custom_column_activate')
.click(this.proxy(this._custom_column_activate));
this.$custom_column.filter('.oe_custom_column_reset')
.click('reset', this.proxy(this._custom_column_diff));
this.$custom_column.filter('.oe_custom_column_all')
.click('to_all', this.proxy(this._custom_column_diff));
this.$custom_column.filter('.oe_custom_column_user')
.click('to_user', this.proxy(this._custom_column_diff));
this.$custom_column.filter('[name="oe_custom_column_field"]')
.change(this.proxy(this._custom_column_add));
this.$('th a.oe_custom_column_left')
.click('left', this.proxy(this._custom_column_diff));
this.$('th a.oe_custom_column_right')
.click('right', this.proxy(this._custom_column_diff));
this.$('th a.oe_custom_column_remove')
.click('remove', this.proxy(this._custom_column_diff));
this.$custom_column.filter('[name="oe_custom_column_field"]')
.find('option')
.each(function(index, option)
{
jQuery(option).prop(
'disabled',
_.any(self.columns, function(column)
{
return column.id == jQuery(option).val();
})
);
});
},
_custom_column_get_element: function()
{
if(this.options.$pager)
{
return this.options.$pager
.siblings('.oe_view_manager_custom_column');
}
else
{
return this.$('.oe_view_manager_custom_column');
}
},
_custom_column_activate: function()
{
if(this.options.custom_column_active)
{
return this._custom_column_deactivate();
}
var deferred = new jQuery.when(),
self = this;
this.options.custom_column_active = true;
if(!this.options.custom_column_fields)
{
deferred = this._custom_column_get_desc();
}
return deferred
.then(this.proxy(this.reload_view))
.then(this.proxy(this.reload_content));
},
_custom_column_get_desc: function()
{
var self = this;
return new instance.web.Model('ir.ui.view')
.call(
'custom_column_desc', [this.fields_view.view_id],
{context: instance.session.user_context}
)
.then(function(desc)
{
self.options.custom_column_fields = desc.fields;
self.options.custom_column_type = desc.type;
});
},
_custom_column_get_fields: function()
{
var fields = this.options.custom_column_fields;
return _.chain(fields).keys().sortBy(function(field)
{
return fields[field].string;
}).value();
},
_custom_column_deactivate: function()
{
this.options.custom_column_active = false;
return this.reload_view().then(this.proxy(this.reload_content));
},
_custom_column_add: function(ev)
{
ev.data = 'add';
return this._custom_column_diff(ev, jQuery(ev.target).val());
},
_custom_column_diff: function(ev, field)
{
ev.stopPropagation();
return new instance.web.Model('ir.ui.view').call(
'custom_column',
[
this.fields_view.view_id,
{
type: this.options.custom_column_type,
operation: ev.data,
name:
field || jQuery(ev.target).parents('th').data('id'),
}
]
)
.then(this.proxy(this._custom_column_get_desc))
.then(this.proxy(this.reload_view))
.then(this.proxy(this.reload_content));
},
});
};

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="ViewManagerAction">
<t t-jquery="ul.oe_view_manager_switch" t-operation="after">
<div class="oe_view_manager_custom_column oe_right" t-attf-style="display: #{widget.active_view == 'list' and 'inline-block' or 'none'}" />
</t>
</t>
<t t-extend="ListView">
<t t-jquery=".oe_list_header_columns th[t-att-data-id] div" t-operation="prepend">
<t t-if="options.custom_column_active">
<a type="button" class="oe_custom_column_remove" title="Remove column">
<i class="fa fa-times" />
</a>
</t>
</t>
<t t-jquery=".oe_list_header_columns th[t-att-data-id] div" t-operation="append">
<t t-if="options.custom_column_active">
<a type="button" class="oe_custom_column_left" title="Move column left" t-if="!column_first">
<i class="fa fa-arrow-left" />
</a>
<a type="button" class="oe_custom_column_right" title="Move column right" t-if="!column_last">
<i class="fa fa-arrow-right" />
</a>
</t>
</t>
</t>
<t t-name="ListView.CustomColumn">
<a type="button" class="oe_custom_column_activate" title="Customize columns">
<i class="fa fa-columns"></i>
</a>
<t t-if="widget.options.custom_column_active">
<select name="oe_custom_column_field">
<option value="" disabled="disabled" selected="selected">
Add column
</option>
<t t-foreach="widget._custom_column_get_fields()" t-as="field">
<option t-att-value="field"><t t-esc="widget.options.custom_column_fields[field].string or '/'" /> (<t t-esc="field" />)</option>
</t>
</select>
<a type="button" t-attf-class="oe_custom_column_user {{widget.options.custom_column_type == 'user' and 'active' or ''}}" title="Customize the list for yourself">
<i class="fa fa-user"></i>
</a>
<a type="button" t-attf-class="oe_custom_column_all {{widget.options.custom_column_type == 'all' and 'active' or ''}}" title="Customize the list for everyone">
<i class="fa fa-users"></i>
</a>
<a type="button" class="oe_custom_column_reset" title="Reset customization">
<i class="fa fa-times"></i>
</a>
</t>
</t>
</templates>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_web_listview_custom_column

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.tests.common import TransactionCase
class TestWebListviewCustomColumn(TransactionCase):
def test_web_listview_custom_column(self):
view = self.env.ref('base.module_tree')
view.custom_column({
'type': 'user', 'operation': 'add', 'name': 'display_name',
})
self.assertIn(
'display_name',
self.env['ir.module.module']
.fields_view_get(view_id=view.id)['arch']
)
view.custom_column({
'type': 'user', 'operation': 'left', 'name': 'display_name',
})
view.custom_column({
'type': 'user', 'operation': 'right', 'name': 'display_name',
})
view.custom_column({
'type': 'user', 'operation': 'remove', 'name': 'display_name',
})
self.assertNotIn(
'display_name',
self.env['ir.module.module']
.fields_view_get(view_id=view.id)['arch']
)
view.custom_column({
'type': 'user', 'operation': 'to_all',
})
self.assertFalse(
self.env.ref(view._custom_column_xmlid({'type': 'user'}), False)
)
self.assertTrue(
self.env.ref(view._custom_column_xmlid({'type': 'all'}))
)
view.custom_column({
'type': 'all', 'operation': 'to_user',
})
self.assertTrue(
self.env.ref(view._custom_column_xmlid({'type': 'all'}))
)
self.assertTrue(
self.env.ref(view._custom_column_xmlid({'type': 'user'}))
)
view.custom_column({
'type': 'user', 'operation': 'reset',
})
self.assertFalse(
self.env.ref(view._custom_column_xmlid({'type': 'user'}), False)
)

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<template id="assets_backend" name="web_listview_custom_column assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_listview_custom_column/static/src/js/web_listview_custom_column.js"></script>
<link rel="stylesheet" href="/web_listview_custom_column/static/src/css/web_listview_custom_column.css"/>
</xpath>
</template>
</data>
</openerp>