[IMP] base, website: add ir.ui.view action to compare arch (wizard)

This commit adds the possibility to compare a view arch to another one.
The result will be shown in a diff viewer (github like).

This is following what was done at #32009

task-2190072

closes odoo/odoo#44646

Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
This commit is contained in:
Romain Derie
2020-03-17 15:58:45 +00:00
committed by Jeremy Kersten
parent 0fc9adbe0b
commit 96d3fa4e01
5 changed files with 138 additions and 68 deletions
+11 -1
View File
@@ -261,7 +261,6 @@
<!-- ir.ui.view views -->
<record model="ir.ui.view" id="view_view_form_extend">
<field name="model">ir.ui.view</field>
<field name="inherit_id" ref="base.view_view_form"/>
<field name="arch" type="xml">
<field name="inherit_id" position="attributes">
@@ -284,6 +283,7 @@
</sheet>
</field>
</record>
<record id="view_view_tree_inherit_website" model="ir.ui.view">
<field name="model">ir.ui.view</field>
<field name="inherit_id" ref="base.view_view_tree"/>
@@ -297,6 +297,16 @@
</field>
</record>
<record id="reset_view_arch_wizard_view" model="ir.ui.view">
<field name="model">reset.view.arch.wizard</field>
<field name="inherit_id" ref="base.reset_view_arch_wizard_view"/>
<field name="arch" type="xml">
<field name="compare_view_id" position="attributes">
<attribute name="context">{'display_website': True}</attribute>
</field>
</field>
</record>
<!-- Dashboard -->
<record id="backend_dashboard" model="ir.actions.client">
<field name="name">Analytics</field>
+64 -57
View File
@@ -14,7 +14,6 @@ import time
import uuid
from dateutil.relativedelta import relativedelta
from difflib import HtmlDiff
import werkzeug, werkzeug.urls
from lxml import etree
@@ -25,7 +24,7 @@ from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError, AccessError
from odoo.http import request
from odoo.modules.module import get_resource_from_path, get_resource_path
from odoo.tools import config, ConstantMapping, pycompat, apply_inheritance_specs, locate_node
from odoo.tools import config, ConstantMapping, get_diff, pycompat, apply_inheritance_specs, locate_node
from odoo.tools.convert import _fix_multiple_roots
from odoo.tools.json import scriptsafe as json_scriptsafe
from odoo.tools.safe_eval import safe_eval
@@ -308,6 +307,9 @@ actual arch.
view_wo_lang.arch = view.arch_base
def reset_arch(self, mode='soft'):
""" Reset the view arch to its previous arch (soft) or its XML file arch
if exists (hard).
"""
for view in self:
arch = False
if mode == 'soft':
@@ -1682,76 +1684,81 @@ actual arch.
class ResetViewArchWizard(models.TransientModel):
""" A wizard to reset views architecture. """
""" A wizard to compare and reset views architecture. """
_name = "reset.view.arch.wizard"
_description = "Reset View Architecture Wizard"
def _default_view_id(self):
view_id = self._context.get('active_model') == 'ir.ui.view' and self._context.get('active_id') or []
return view_id
view_id = fields.Many2one('ir.ui.view', string='View', default=_default_view_id)
view_id = fields.Many2one('ir.ui.view', string='View')
view_name = fields.Char(related='view_id.name', string='View Name')
arch_diff = fields.Html(string='Architecture Diff', compute='_compute_arch_diff', readonly=True, sanitize_tags=False)
has_diff = fields.Boolean(compute='_compute_arch_diff')
arch_diff = fields.Html(string='Architecture Diff', readonly=True,
compute='_compute_arch_diff', sanitize_tags=False)
reset_mode = fields.Selection([
('soft', 'Restore previous version (soft reset).'),
('hard', 'Reset to file version (hard reset).')
], string='Reset Mode', default='soft', required=True, help="You might want to try a soft reset first.")
('hard', 'Reset to file version (hard reset).'),
('other_view', 'Reset to another view.')],
string='Reset Mode', default='soft', required=True)
compare_view_id = fields.Many2one('ir.ui.view', string='Compare To View')
arch_to_compare = fields.Text('Arch To Compare To', compute='_compute_arch_diff')
@api.depends('reset_mode', 'view_id')
@api.model
def default_get(self, fields):
view_ids = (self._context.get('active_model') == 'ir.ui.view' and
self._context.get('active_ids') or [])
if len(view_ids) > 2:
raise ValidationError(_("Can't compare more than two views."))
result = super().default_get(fields)
result['view_id'] = view_ids and view_ids[0]
if len(view_ids) == 2:
result['reset_mode'] = 'other_view'
result['compare_view_id'] = view_ids[1]
return result
@api.depends('reset_mode', 'view_id', 'compare_view_id')
def _compute_arch_diff(self):
''' Return the differences between the current view arch and either its
previous or initial arch, depending of `reset_mode` (soft/hard).
The diff will be returned in an HTML table like on github.com.
'''
def handle_style(html_diff):
''' The HtmlDiff lib will add some usefull classes on the DOM to
identify elements. Simply replace those classes by BS4 ones.
For the table to fit the modal width, some custom style is needed.
'''
to_replace = {
'diff_header': 'diff_header bg-600 text-center align-top px-2',
'diff_next': 'd-none',
'diff_add': 'bg-success',
'diff_chg': 'bg-warning',
'diff_sub': 'bg-danger',
'nowrap': '',
}
for old, new in to_replace.items():
html_diff = html_diff.replace(old, new)
html_diff += '''
<style>
table.diff { width: 100%; }
table.diff .diff_header { white-space: nowrap; }
table.diff th.diff_header { width: 50%; }
table.diff td { word-break: break-all; }
</style>
'''
return html_diff
""" Depending of `reset_mode`, return the differences between the
current view arch and either its previous arch, its initial arch or
another view arch.
"""
def get_table_name(view_id):
name = view_id.display_name
if view_id.key or view_id.xml_id:
span = '<span class="ml-1 font-weight-normal small">(%s)</span>'
name += span % (view_id.key or view_id.xml_id)
return name
for view in self:
soft = view.reset_mode == 'soft'
arch_to_compare = False
if soft:
arch_to_compare = view.view_id.arch_prev
elif not soft and view.view_id.arch_fs:
arch_to_compare = view.view_id.with_context(read_arch_from_file=True).arch
diff_to = False
diff_to_name = False
if view.reset_mode == 'soft':
diff_to = view.view_id.arch_prev
diff_to_name = _("Previous Arch")
elif view.reset_mode == 'other_view':
diff_to = view.compare_view_id.arch
diff_to_name = get_table_name(view.compare_view_id)
elif view.reset_mode == 'hard' and view.view_id.arch_fs:
diff_to = view.view_id.with_context(read_arch_from_file=True).arch
diff_to_name = _("File Arch")
diff = False
if arch_to_compare:
diff = HtmlDiff(tabsize=2).make_table(
arch_to_compare.splitlines(),
view.view_id.arch.splitlines(),
_("Previous Arch") if soft else _("File Arch"),
_("Current Arch"),
context=True, # Show only diff lines, not all the code
view.arch_to_compare = diff_to
if not diff_to:
view.arch_diff = False
view.has_diff = False
else:
view.arch_diff = get_diff(
(view.view_id.arch, get_table_name(view.view_id) if view.reset_mode == 'other_view' else _("Current Arch")),
(diff_to, diff_to_name),
)
diff = handle_style(diff)
view.arch_diff = diff
view.has_diff = view.view_id.arch != diff_to
def reset_view_button(self):
self.ensure_one()
self.view_id.reset_arch(self.reset_mode)
if self.reset_mode == 'other_view':
self.view_id.write({'arch_db': self.arch_to_compare})
else:
self.view_id.reset_arch(self.reset_mode)
return {'type': 'ir.actions.act_window_close'}
@@ -33,6 +33,7 @@
"access_ir_ui_menu_group_system","ir_ui_menu group_system","model_ir_ui_menu","group_system",1,1,1,1
"access_ir_ui_view_group_user","ir_ui_view group_user","model_ir_ui_view",,1,0,0,0
"access_ir_ui_view_group_system","ir_ui_view group_system","model_ir_ui_view","group_system",1,1,1,1
"access_reset_view_arch_wizard_group_system","reset_view_arch_wizard group_system","model_reset_view_arch_wizard","group_system",1,1,1,0
"access_ir_ui_view_custom_group_user","ir_ui_view_custom_group_user","model_ir_ui_view_custom",,1,1,1,1
"access_ir_default_all","ir_default all","model_ir_default",,1,0,0,0
"access_ir_default_group_user","ir_default group_user","model_ir_default","group_user",1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
33 access_ir_ui_menu_group_system ir_ui_menu group_system model_ir_ui_menu group_system 1 1 1 1
34 access_ir_ui_view_group_user ir_ui_view group_user model_ir_ui_view 1 0 0 0
35 access_ir_ui_view_group_system ir_ui_view group_system model_ir_ui_view group_system 1 1 1 1
36 access_reset_view_arch_wizard_group_system reset_view_arch_wizard group_system model_reset_view_arch_wizard group_system 1 1 1 0
37 access_ir_ui_view_custom_group_user ir_ui_view_custom_group_user model_ir_ui_view_custom 1 1 1 1
38 access_ir_default_all ir_default all model_ir_default 1 0 0 0
39 access_ir_default_group_user ir_default group_user model_ir_default group_user 1 1 1 1
+16 -10
View File
@@ -106,18 +106,24 @@
<field name="arch" type="xml">
<form string="Reset View Architecture">
<group>
<field name="view_id" invisible="1"/>
<field name="view_name"/>
<field name="reset_mode" widget="radio"/>
<group>
<field name="has_diff" invisible="1"/>
<field name="view_id" invisible="1"/>
<field name="view_name"/>
<field name="compare_view_id" attrs="{'invisible': [('reset_mode', '!=', 'other_view')]}" options="{'no_create': True, 'no_open': True}"/>
</group>
<group>
<field name="reset_mode" widget="radio"/>
</group>
</group>
<field name="arch_diff" attrs="{'invisible': [('arch_diff', '=', False)]}"/>
<div class="text-warning mb4 mt16" attrs="{'invisible': [('arch_diff', '!=', False)]}">
<span attrs="{'invisible': [('reset_mode', '=', 'hard')]}">This view has no previous version.</span>
<span attrs="{'invisible': [('reset_mode', '=', 'soft')]}">This view is not coming from a file.</span>
<div class="alert alert-warning my-2" role="alert" attrs="{'invisible': [('arch_diff', '!=', False)]}">
<span attrs="{'invisible': [('reset_mode', '!=', 'soft')]}">This view has no previous version.</span>
<span attrs="{'invisible': [('reset_mode', '!=', 'hard')]}">This view is not coming from a file.</span>
<span attrs="{'invisible': [('reset_mode', '!=', 'other_view')]}">You need two views to compare.</span>
</div>
<footer>
<button string="Reset View" name="reset_view_button" type="object" class="btn-primary" attrs="{'invisible': [('arch_diff', '=', False)]}"/>
<button string="Reset View" name="reset_view_button" type="object" class="btn-primary" disabled="disabled" attrs="{'invisible': [('arch_diff', '!=', False)]}"/>
<button string="Reset View" name="reset_view_button" type="object" class="btn-primary" attrs="{'invisible': [('has_diff', '=', False)]}"/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
@@ -125,8 +131,8 @@
</record>
<act_window id="reset_view_arch_wizard_action"
name="Reset View Architecture"
binding_model="ir.ui.view" binding_views="form"
name="Compare/Reset"
binding_model="ir.ui.view" binding_views="form,list"
res_model="reset.view.arch.wizard"
view_mode="form"
target="new"/>
+46
View File
@@ -29,6 +29,7 @@ import unicodedata
import werkzeug.utils
import zipfile
from collections import defaultdict, Iterable, Mapping, MutableMapping, MutableSet, OrderedDict
from difflib import HtmlDiff
from itertools import islice, groupby as itergroupby, repeat
from lxml import etree
@@ -1470,3 +1471,48 @@ class DotDict(dict):
def __getattr__(self, attrib):
val = self.get(attrib)
return DotDict(val) if type(val) is dict else val
def get_diff(data_from, data_to, custom_style=False):
"""
Return, in an HTML table, the diff between two texts.
:param tuple data_from: tuple(text, name), name will be used as table header
:param tuple data_to: tuple(text, name), name will be used as table header
:param tuple custom_style: string, style css including <style> tag.
:return: a string containing the diff in an HTML table format.
"""
def handle_style(html_diff, custom_style):
""" The HtmlDiff lib will add some usefull classes on the DOM to
identify elements. Simply append to those classes some BS4 ones.
For the table to fit the modal width, some custom style is needed.
"""
to_append = {
'diff_header': 'bg-600 text-center align-top px-2',
'diff_next': 'd-none',
'diff_add': 'bg-success',
'diff_chg': 'bg-warning',
'diff_sub': 'bg-danger',
}
for old, new in to_append.items():
html_diff = html_diff.replace(old, "%s %s" % (old, new))
html_diff = html_diff.replace('nowrap', '')
html_diff += custom_style or '''
<style>
table.diff { width: 100%; }
table.diff th.diff_header { width: 50%; }
table.diff td.diff_header { white-space: nowrap; }
table.diff td { word-break: break-all; }
</style>
'''
return html_diff
diff = HtmlDiff(tabsize=2).make_table(
data_from[0].splitlines(),
data_to[0].splitlines(),
data_from[1],
data_to[1],
context=True, # Show only diff lines, not all the code
numlines=3,
)
return handle_style(diff, custom_style)