diff options
| author | Romain Derie <rde@odoo.com> | 2020-02-04 18:17:45 +0000 |
|---|---|---|
| committer | Jeremy Kersten <jke@odoo.com> | 2020-03-17 15:58:45 +0000 |
| commit | 96d3fa4e01bf8afc35dbf0b7301ce75c6bf3a5c7 (patch) | |
| tree | 0059c56e41a7a48151e0960bd8ea9b6eeda0d9f0 | |
| parent | 0fc9adbe0b20ef783c99dba1810bb534e6fbb216 (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.xml | 12 | ||||
| -rw-r--r-- | odoo/addons/base/models/ir_ui_view.py | 123 | ||||
| -rw-r--r-- | odoo/addons/base/security/ir.model.access.csv | 1 | ||||
| -rw-r--r-- | odoo/addons/base/views/ir_ui_view_views.xml | 26 | ||||
| -rw-r--r-- | odoo/tools/misc.py | 46 |
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) |
