summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRomain Derie <rde@odoo.com>2020-02-04 18:17:45 +0000
committerJeremy Kersten <jke@odoo.com>2020-03-17 15:58:45 +0000
commit96d3fa4e01bf8afc35dbf0b7301ce75c6bf3a5c7 (patch)
tree0059c56e41a7a48151e0960bd8ea9b6eeda0d9f0
parent0fc9adbe0b20ef783c99dba1810bb534e6fbb216 (diff)
[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>
-rw-r--r--addons/website/views/website_views.xml12
-rw-r--r--odoo/addons/base/models/ir_ui_view.py123
-rw-r--r--odoo/addons/base/security/ir.model.access.csv1
-rw-r--r--odoo/addons/base/views/ir_ui_view_views.xml26
-rw-r--r--odoo/tools/misc.py46
5 files changed, 139 insertions, 69 deletions
diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml
index c3fa50a2c96..a009193c25c 100644
--- a/addons/website/views/website_views.xml
+++ b/addons/website/views/website_views.xml
@@ -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>
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index efa4dcce214..4bcb1e6873d 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -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 = 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
+ 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")
+
+ 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'}
diff --git a/odoo/addons/base/security/ir.model.access.csv b/odoo/addons/base/security/ir.model.access.csv
index 211e9d5485c..861ebbf5aac 100644
--- a/odoo/addons/base/security/ir.model.access.csv
+++ b/odoo/addons/base/security/ir.model.access.csv
@@ -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
diff --git a/odoo/addons/base/views/ir_ui_view_views.xml b/odoo/addons/base/views/ir_ui_view_views.xml
index 7f83fde66b7..b2881334979 100644
--- a/odoo/addons/base/views/ir_ui_view_views.xml
+++ b/odoo/addons/base/views/ir_ui_view_views.xml
@@ -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"/>
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 534493afa95..b19e3b44923 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -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)