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
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
Test View Child Broken
+
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('')
+ # self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (1)")
+ # self.fix_it('/test_view')
+
+ # also mute ir.ui.view as `get_view_id()` will raise "Could not find view object with xml_id 'not.exist'""
+ @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view')
+ def test_06_reset_specific_view_controller_inexisting_template(self):
+ total_views = self.View.search_count([('type', '=', 'qweb')])
+ # Trigger COW then break the QWEB XML on it
+ break_view(self.test_view.with_context(website_id=1), to='')
+ self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)")
+ self.fix_it('/test_view')
+
+ @mute_logger('odoo.addons.website.models.ir_http')
+ def test_07_reset_page_view_complete_flow(self):
+ self.do_test('test_reset_page_view_complete_flow_part1')
+ self.fix_it('/test_page_view')
+ self.do_test('test_reset_page_view_complete_flow_part2')
+ self.fix_it('/test_page_view')
diff --git a/addons/test_website/views/templates.xml b/addons/test_website/views/templates.xml
new file mode 100644
index 00000000000..2ce171bac30
--- /dev/null
+++ b/addons/test_website/views/templates.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 6f1a2df1e8e..81d461830ef 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -286,24 +286,36 @@ class Website(Home):
views = views.sorted(key=lambda v: (v.inherit_id.id, v.name))
return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
- @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True)
+ @http.route('/website/reset_templates', type='http', auth='user', methods=['POST'], website=True, csrf=False)
def reset_template(self, templates, redirect='/', **kwargs):
+ """ This method will try to reset a list of broken views ids.
+ It will read the original `arch` from the view's XML file (arch_fs).
+ Views without an `arch_fs` can't be reset, except views created when
+ dropping a snippet in specific oe_structure that create an inherited
+ view doing an xpath.
+ Note: The `arch_fs` field is automatically erased when there is a
+ write on the `arch` field.
+
+ This method is typically useful to reset specific views. In that case we
+ read the XML file from the generic view.
+ """
templates = request.httprequest.form.getlist('templates')
- modules_to_update = []
for temp_id in templates:
view = request.env['ir.ui.view'].browse(int(temp_id))
- if view.page:
+ if 'oe_structure' in view.key:
+ # Particular xpathing view created in edit mode
+ view.unlink()
continue
- view.model_data_id.write({
- 'noupdate': False
- })
- if view.model_data_id.module not in modules_to_update:
- modules_to_update.append(view.model_data_id.module)
+ xml_view = view._get_original_view() # view might already be the xml_view
+ if xml_view.arch_fs:
+ view_file_arch = xml_view.with_context(read_arch_from_file=True).arch
+ # Deactivate COW to not fix a generic view by creating a specific
+ view.with_context(website_id=None).arch_db = view_file_arch
+ if view == xml_view:
+ view.model_data_id.write({
+ 'noupdate': False
+ })
- if modules_to_update:
- modules = request.env['ir.module.module'].sudo().search([('name', 'in', modules_to_update)])
- if modules:
- modules.button_immediate_upgrade()
return request.redirect(redirect)
@http.route(['/website/publish'], type='json', auth="public", website=True)
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 49e06997692..c45b9ff20fe 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
+from lxml import etree
import traceback
import os
import unittest
@@ -12,14 +13,14 @@ import werkzeug.routing
import werkzeug.utils
import odoo
-from odoo import api, models
+from odoo import api, models, registry
from odoo import SUPERUSER_ID
from odoo.http import request
from odoo.tools import config
-from odoo.exceptions import QWebException
from odoo.tools.safe_eval import safe_eval
from odoo.osv.expression import FALSE_DOMAIN, OR
+from odoo.addons.base.models.qweb import QWebException
from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype
from odoo.addons.portal.controllers.portal import _build_url_w_params
@@ -185,7 +186,7 @@ class Http(models.AbstractModel):
# if parent excplicitely returns a plain response, then we don't touch it
return response
except Exception as e:
- if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, QWebException) or not exception.qweb.get('cause')):
+ if 'werkzeug' in config['dev_mode']:
raise e
exception = e
@@ -207,19 +208,9 @@ class Http(models.AbstractModel):
if isinstance(exception, QWebException):
values.update(qweb_exception=exception)
- if isinstance(exception.qweb.get('cause'), odoo.exceptions.AccessError):
+ if type(exception.error) == odoo.exceptions.AccessError:
code = 403
- if code == 500:
- logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
- if 'qweb_exception' in values:
- view = request.env["ir.ui.view"]
- views = view._views_get(exception.qweb['template'])
- to_reset = views.filtered(lambda view: view.model_data_id.noupdate is True and view.arch_fs)
- values['views'] = to_reset
- elif code == 403:
- logger.warn("403 Forbidden:\n\n%s", values['traceback'])
-
values.update(
status_message=werkzeug.http.HTTP_STATUS_CODES[code],
status_code=code,
@@ -233,10 +224,51 @@ class Http(models.AbstractModel):
if not request.uid:
cls._auth_method_public()
- try:
- html = request.env['ir.ui.view'].render_template('website.%s' % view_id, values)
- except Exception:
- html = request.env['ir.ui.view'].render_template('website.http_error', values)
+ with registry(request.env.cr.dbname).cursor() as cr:
+ env = api.Environment(cr, SUPERUSER_ID, request.env.context)
+ if code == 500:
+ logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
+ View = env["ir.ui.view"]
+ if 'qweb_exception' in values:
+ if 'load could not load template' in exception.args:
+ # When t-calling an inexisting template, we don't have reference to
+ # the view that did the t-call. We need to find it.
+ values['views'] = View.search([
+ ('type', '=', 'qweb'),
+ '|',
+ ('arch_db', 'ilike', 't-call="%s"' % exception.name),
+ ('arch_db', 'ilike', "t-call='%s'" % exception.name)
+ ], order='write_date desc', limit=1)
+ else:
+ try:
+ # exception.name might be int, string
+ exception_template = int(exception.name)
+ except:
+ exception_template = exception.name
+ view = View._view_obj(exception_template)
+ et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch'])
+ node = et.find(exception.path.replace('/templates/t/', './'))
+ line = node is not None and etree.tostring(node, encoding='unicode')
+ # line = exception.html # FALSE -> contains branding
+ if line:
+ # If QWebException occurs in a child view, the parent view is raised
+ values['editable'] = request.uid and request.website.is_publisher()
+ values['views'] = View._views_get(exception_template).filtered(
+ lambda v: line in v.arch
+ )
+ else:
+ values['views'] = view
+ # Keep only views that we can reset
+ values['views'] = values['views'].filtered(
+ lambda view: view._get_original_view().arch_fs or 'oe_structure' in view.key
+ )
+ elif code == 403:
+ logger.warn("403 Forbidden:\n\n%s", values['traceback'])
+ try:
+ html = env['ir.ui.view'].render_template('website.%s' % view_id, values)
+ except Exception:
+ html = env['ir.ui.view'].render_template('website.http_error', values)
+
return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
@classmethod
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 977a986745b..463aed48f84 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -278,6 +278,16 @@ class View(models.Model):
return view.id
return super(View, self).get_view_id(xml_id)
+ @api.multi
+ def _get_original_view(self):
+ """Given a view, retrieve the original view it was COW'd from.
+ The given view might already be the original one. In that case it will
+ (and should) return itself.
+ """
+ self.ensure_one()
+ domain = [('key', '=', self.key), ('model_data_id', '!=', None)]
+ return self.search(domain, limit=1) # Useless limit has multiple xmlid should not be possible
+
@api.multi
def render(self, values=None, engine='ir.qweb', minimal_qcontext=False):
""" Render the template. If website is enabled on request, then extend rendering context with website values. """
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 9abc878412e..cc7cff9ca93 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -870,17 +870,14 @@
-
+
Error message:
-
+
- The error occured while rendering the template
- and evaluating the following expression:
+ The error occured while rendering the template
+ and evaluating the following expression: