Browse Source

[ADD] web_listview_custom_column (#787)

pull/158/head
Holger Brunn 6 years ago
committed by Pedro M. Baeza
parent
commit
9128286df7
  1. 1
      oca_dependencies.txt
  2. 79
      web_listview_custom_column/README.rst
  3. 4
      web_listview_custom_column/__init__.py
  4. 23
      web_listview_custom_column/__openerp__.py
  5. 4
      web_listview_custom_column/models/__init__.py
  6. 143
      web_listview_custom_column/models/ir_ui_view.py
  7. 2
      web_listview_custom_column/security/ir.model.access.csv
  8. 12
      web_listview_custom_column/security/res_groups.xml
  9. BIN
      web_listview_custom_column/static/description/icon.png
  10. 40
      web_listview_custom_column/static/src/css/web_listview_custom_column.css
  11. 136
      web_listview_custom_column/static/src/js/web_listview_custom_column.js
  12. 51
      web_listview_custom_column/static/src/xml/web_listview_custom_column.xml
  13. 4
      web_listview_custom_column/tests/__init__.py
  14. 55
      web_listview_custom_column/tests/test_web_listview_custom_column.py
  15. 11
      web_listview_custom_column/views/templates.xml

1
oca_dependencies.txt

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

79
web_listview_custom_column/README.rst

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

4
web_listview_custom_column/__init__.py

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

23
web_listview_custom_column/__openerp__.py

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

4
web_listview_custom_column/models/__init__.py

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

143
web_listview_custom_column/models/ir_ui_view.py

@ -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_'
)
]

2
web_listview_custom_column/security/ir.model.access.csv

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

12
web_listview_custom_column/security/res_groups.xml

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

BIN
web_listview_custom_column/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

40
web_listview_custom_column/static/src/css/web_listview_custom_column.css

@ -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;
}

136
web_listview_custom_column/static/src/js/web_listview_custom_column.js

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

51
web_listview_custom_column/static/src/xml/web_listview_custom_column.xml

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

4
web_listview_custom_column/tests/__init__.py

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

55
web_listview_custom_column/tests/test_web_listview_custom_column.py

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

11
web_listview_custom_column/views/templates.xml

@ -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>
Loading…
Cancel
Save