From ef03db9edd9472201cb2c08a32d20ff0f33a5fdf Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Fri, 28 Dec 2018 15:24:33 +0000 Subject: [PATCH] [FIX] website: restore reset_templates functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since AST was introduced to compile Qweb templates, the reset templates functionality is not working anymore. Actually, it does not even appear anymore as the conditions to trigger and show the reset view behavior can't be True anymore. Indeed, the code is still importing the old QWebException from odoo.exceptions which is now just an empty class. The QWebException that should now be used is the one from odoo.base.models.qweb More than just a wrong import, the QWebException properties have completely changed. Since its last working version (Odoo 9.0), a lot of changes occured, mostly: - Odoo 11.0: new model website.page, which completely changed the pages behavior and changed the models. - Odoo 12.0: multi-website, which introduced the Copy On Write (COW) that redefined how we write on view in a website context. We now need to reset copied/specific views that have no model_data_id. This fix is for a stable version. There is some flows that can't be fixed without a refactoring or new fields (mainly because of the arch_fs being erased when writing on `arch`, plus we can't introduce a module reload in stable). The goal of this commit is to fix the basic and standard case of broken views, which is when non-technical users are breaking the views through HTML editor. To reset the views, we will read the original arch in the XML file, as the dev mode is doing. To be eligible for the reset, the broken view need an `arch_fs` set. If the view arch has been modified anywhere else than the frontend, the `arch_fs` will be erased and the view wont be able to be reset. As the broken views will be specific views, we will simply read the generic view in the XML file and write it on the specific. We can't simply remove the specific view as we might be on a specific tree. Specific case, in case of custom views create dropping a snippet in an oe_struc we just delete this inehrited view to allow end user to have their page back. In some case of bad compilation in sub template, we try to guess which inherited view, t-called view is broken by string matching based on 'last_path_node' xpath. Add new module test_website to add tests, it will be usefull for futur test of install/uninstall and ... Todo in master: - try to track on QWebException the real template that is broken and not only the last node from the main template. Co-authored-by: "Romain Derie " Co-authored-by: "Jérémy Kersten " closes odoo/odoo#29957 --- addons/test_website/__init__.py | 4 + addons/test_website/__manifest__.py | 24 ++++ addons/test_website/controllers/__init__.py | 4 + addons/test_website/controllers/main.py | 13 ++ .../test_website/data/test_website_data.xml | 54 ++++++++ .../static/src/js/reset_views.test.js | 103 +++++++++++++++ addons/test_website/tests/__init__.py | 4 + addons/test_website/tests/test_reset_views.py | 102 +++++++++++++++ addons/test_website/views/templates.xml | 8 ++ addons/website/controllers/main.py | 36 +++-- addons/website/models/ir_http.py | 68 +++++++--- addons/website/models/ir_ui_view.py | 10 ++ addons/website/views/website_templates.xml | 123 ++++++++++-------- odoo/addons/base/models/ir_ui_view.py | 2 +- 14 files changed, 468 insertions(+), 87 deletions(-) create mode 100644 addons/test_website/__init__.py create mode 100644 addons/test_website/__manifest__.py create mode 100644 addons/test_website/controllers/__init__.py create mode 100644 addons/test_website/controllers/main.py create mode 100644 addons/test_website/data/test_website_data.xml create mode 100644 addons/test_website/static/src/js/reset_views.test.js create mode 100644 addons/test_website/tests/__init__.py create mode 100644 addons/test_website/tests/test_reset_views.py create mode 100644 addons/test_website/views/templates.xml diff --git a/addons/test_website/__init__.py b/addons/test_website/__init__.py new file mode 100644 index 00000000000..273ede25ea3 --- /dev/null +++ b/addons/test_website/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py new file mode 100644 index 00000000000..557722971d4 --- /dev/null +++ b/addons/test_website/__manifest__.py @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Website Test', + 'version': '1.0', + 'category': 'Hidden', + 'sequence': 9876, + 'summary': 'Website Test, mainly for module install/uninstall tests', + 'description': """This module contains tests related to website. Those are +contained in a separate module as we are testing module install/uninstall/upgrade +and we don't want to reload the website module every time, including it's possible +dependencies. Neither we want to add in website module some routes, views and +models which only purpose is to run tests.""", + 'depends': [ + 'website', + ], + 'data': [ + 'views/templates.xml', + 'data/test_website_data.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/addons/test_website/controllers/__init__.py b/addons/test_website/controllers/__init__.py new file mode 100644 index 00000000000..5d4b25db9c0 --- /dev/null +++ b/addons/test_website/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/test_website/controllers/main.py b/addons/test_website/controllers/main.py new file mode 100644 index 00000000000..fad797d6984 --- /dev/null +++ b/addons/test_website/controllers/main.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request +from odoo.addons.portal.controllers.web import Home + + +class WebsiteTest(Home): + + @http.route('/test_view', type='http', auth="public", website=True) + def test_view(self, **kw): + return request.render('test_website.test_view') diff --git a/addons/test_website/data/test_website_data.xml b/addons/test_website/data/test_website_data.xml new file mode 100644 index 00000000000..df827d542a3 --- /dev/null +++ b/addons/test_website/data/test_website_data.xml @@ -0,0 +1,54 @@ + + + + + Test View + qweb + test_website.test_view + + + +

Test View

+

placeholder

+
+
+
+
+ + Test Page View + qweb + test_website.test_page_view + + + +
+

Test Page View

+

placeholder

+ + + + + + True + /test_page_view + + + + Test View To Be t-called + qweb + test_website.test_view_to_be_t_called + + +

Test View To Be t-called

+

placeholder

+
+
+
+ + + diff --git a/addons/test_website/static/src/js/reset_views.test.js b/addons/test_website/static/src/js/reset_views.test.js new file mode 100644 index 00000000000..957cda6d594 --- /dev/null +++ b/addons/test_website/static/src/js/reset_views.test.js @@ -0,0 +1,103 @@ +odoo.define('test_website.reset_views', function (require) { +'use strict'; + +var tour = require("web_tour.tour"); +var base = require("web_editor.base"); + +tour.register('test_reset_page_view_complete_flow_part1', { + test: true, + url: '/test_page_view', + wait_for: base.ready() +}, + [ + // 1. Edit the page through Edit Mode, it will COW the view + { + content: "enter edit mode", + trigger: "a[data-action=edit]" + }, + { + content: "drop a snippet", + trigger: "#oe_snippets .oe_snippet:has(.s_cover) .oe_snippet_thumbnail", + // id starting by 'oe_structure..' will actually create an inherited view + run: "drag_and_drop #oe_structure_test_website_page", + }, + { + content: "save the page", + extra_trigger: '#oe_structure_test_website_page .s_cover', + trigger: "#web_editor-top-edit button[data-action=save]", + }, + // 2. Edit that COW'd view in the HTML editor to break it. + { + content: "open customize menu", + extra_trigger: "body:not(.editor_enable)", + trigger: '#customize-menu > a', + }, + { + content: "open html editor", + trigger: '#html_editor', + }, + { + content: "add a broken t-field in page DOM", + trigger: 'div.ace_line .ace_xml:contains("placeholder")', + run: function () { + ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '\n'); + }, + }, + { + content: "save the html editor", + extra_trigger: '.ace_content:contains("not.exist")', + trigger: ".o_ace_view_editor button[data-action=save]", + } + + // 3. Reset the broken view + ] +); + +tour.register('test_reset_page_view_complete_flow_part2', { + test: true, + url: '/test_page_view', + wait_for: base.ready() +}, + [ + { + content: "check that the view got fixed", + trigger: 'p:containsExact("Test Page View")', + run: function () {}, // it's a check + }, + { + content: "check that the inherited COW view is still there (created during edit mode)", + trigger: '#oe_structure_test_website_page .s_cover', + run: function () {}, // it's a check + }, + //4. Now break the inherited view created when dropping a snippet + { + content: "open customize menu", + trigger: '#customize-menu > a', + }, + { + content: "open html editor", + trigger: '#html_editor', + }, + { + content: "select oe_structure view", + trigger: '#ace-view-list', + run: function () { + var viewId = $('#ace-view-list option:contains("oe_structure")').val(); + $('#ace-view-list').val(viewId).trigger('change'); + }, + }, + { + content: "add a broken t-field in page DOM", + trigger: 'div.ace_line .ace_xml:contains("oe_structure_test_website_page")', + run: function () { + ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '\n'); + }, + }, + { + content: "save the html editor", + trigger: ".o_ace_view_editor button[data-action=save]", + } + ] +); + +}); diff --git a/addons/test_website/tests/__init__.py b/addons/test_website/tests/__init__.py new file mode 100644 index 00000000000..4c32d5f1959 --- /dev/null +++ b/addons/test_website/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_reset_views diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py new file mode 100644 index 00000000000..047b1abe3cd --- /dev/null +++ b/addons/test_website/tests/test_reset_views.py @@ -0,0 +1,102 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import re + +import odoo.tests +from odoo.tools import mute_logger + + +def break_view(view, fr='

placeholder

', to='

'): + view.arch = view.arch.replace(fr, to) + + +@odoo.tests.common.tagged('post_install', '-at_install') +class TestWebsiteResetViews(odoo.tests.HttpCase): + def do_test(self, name): + self.browser_js( + "/", + "odoo.__DEBUG__.services['web_tour.tour'].run('%s')" % name, + "odoo.__DEBUG__.services['web_tour.tour'].tours.%s.ready" % name, + login="admin" + ) + + def fix_it(self, page): + self.authenticate("admin", "admin") + resp = self.url_open(page) + self.assertEqual(resp.status_code, 500, "Waiting 500") + self.assertTrue('