[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:
committed by
Jeremy Kersten
parent
0fc9adbe0b
commit
96d3fa4e01
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user