[FIX] website: restore reset_templates functionality
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 <rde@odoo.com>"
Co-authored-by: "Jérémy Kersten <jke@openerp.com>"
closes odoo/odoo#29957
This commit is contained in:
committed by
Jeremy Kersten
co-authored by
"Romain Derie <rde@odoo.com>"
"Jérémy Kersten <jke@openerp.com>"
parent
01d925e052
commit
ef03db9edd
@@ -0,0 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
@@ -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')
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="test_view" model="ir.ui.view">
|
||||
<field name="name">Test View</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_view</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test View" priority="29" t-name="test_website.test_view">
|
||||
<t t-call="website.layout">
|
||||
<p>Test View</p>
|
||||
<p>placeholder</p>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_page_view" model="ir.ui.view">
|
||||
<field name="name">Test Page View</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_page_view</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test Page View" priority="29" t-name="test_website.test_page_view">
|
||||
<t t-call="website.layout">
|
||||
<div id="oe_structure_test_website_page" class="oe_structure oe_empty"/>
|
||||
<p>Test Page View</p>
|
||||
<p>placeholder</p>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/test_page_view</field>
|
||||
<field name="view_id" ref="test_page_view"/>
|
||||
</record>
|
||||
<record id="test_view_to_be_t_called" model="ir.ui.view">
|
||||
<field name="name">Test View To Be t-called</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_view_to_be_t_called</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test View To Be t-called" priority="29" t-name="test_website.test_view_to_be_t_called">
|
||||
<p>Test View To Be t-called</p>
|
||||
<p>placeholder</p>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<template id="test_view_child_broken" inherit_id="test_website.test_view" active="False">
|
||||
<xpath expr="//p[last()]" position="replace">
|
||||
<p>Test View Child Broken</p>
|
||||
<p>placeholder</p>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -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}, '<t t-field="not.exist"/>\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}, '<t t-field="not.exist"/>\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "save the html editor",
|
||||
trigger: ".o_ace_view_editor button[data-action=save]",
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_reset_views
|
||||
@@ -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='<p>placeholder</p>', to='<p t-field="not.exist"/>'):
|
||||
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('<button id="reset_templates_button"' in resp.text)
|
||||
data = {'templates': [self.find_template(resp)], 'redirect': page}
|
||||
resp = self.url_open('/website/reset_templates', data)
|
||||
self.assertEqual(resp.status_code, 200, "Waiting 200")
|
||||
|
||||
def find_template(self, response):
|
||||
find = re.search(r'<input.*type="checkbox".*name="templates".*value="([0-9]+)?"', response.text)
|
||||
return find and find.group(1)
|
||||
|
||||
def setUp(self):
|
||||
super(TestWebsiteResetViews, self).setUp()
|
||||
self.Website = self.env['website']
|
||||
self.View = self.env['ir.ui.view']
|
||||
self.test_view = self.Website.viewref('test_website.test_view')
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http')
|
||||
def test_01_reset_specific_page_view(self):
|
||||
self.test_page_view = self.Website.viewref('test_website.test_page_view')
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
break_view(self.test_page_view.with_context(website_id=1))
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
self.fix_it('/test_page_view')
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http')
|
||||
def test_02_reset_specific_view_controller(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))
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
self.fix_it('/test_view')
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http')
|
||||
def test_03_reset_specific_view_controller_t_called(self):
|
||||
self.test_view_to_be_t_called = self.Website.viewref('test_website.test_view_to_be_t_called')
|
||||
|
||||
total_views = self.View.search_count([('type', '=', 'qweb')])
|
||||
# Trigger COW then break the QWEB XML on it
|
||||
break_view(self.test_view_to_be_t_called.with_context(website_id=1))
|
||||
break_view(self.test_view, to='<t t-call="test_website.test_view_to_be_t_called"/>')
|
||||
self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
|
||||
self.fix_it('/test_view')
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http')
|
||||
def test_04_reset_specific_view_controller_inherit(self):
|
||||
self.test_view_child_broken = self.Website.viewref('test_website.test_view_child_broken')
|
||||
|
||||
# Activate and break the inherited view
|
||||
self.test_view_child_broken.active = True
|
||||
break_view(self.test_view_child_broken.with_context(website_id=1, load_all_views=True))
|
||||
|
||||
self.fix_it('/test_view')
|
||||
|
||||
# This test work in real life, but not in test mode since we cannot rollback savepoint.
|
||||
# @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view')
|
||||
# def test_05_reset_specific_view_controller_broken_request(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='<t t-esc="request.env[\'website\'].browse(\'a\').name" />')
|
||||
# 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='<t t-call="not.exist"/>')
|
||||
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')
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend">
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/test_website/static/src/js/reset_views.test.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <div t-att-data="request.browse('ok')"/>
|
||||
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
|
||||
|
||||
@@ -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. """
|
||||
|
||||
@@ -870,17 +870,14 @@
|
||||
</h4>
|
||||
<div id="error_qweb" class="collapse">
|
||||
<div class="card-body">
|
||||
<p t-if="exception.qweb.get('message')">
|
||||
<p t-if="exception.message">
|
||||
<strong>Error message:</strong>
|
||||
<pre t-esc="exception.qweb.get('message')"/>
|
||||
<pre t-esc="exception.message"/>
|
||||
</p>
|
||||
<p>
|
||||
The error occured while rendering the template <code t-esc="qweb_exception.qweb.get('template')"/>
|
||||
<t t-if="'expression' in qweb_exception.qweb">and evaluating the following expression: <code t-esc="qweb_exception.qweb['expression']"/></t>
|
||||
The error occured while rendering the template <code t-esc="qweb_exception.name"/>
|
||||
<t t-if="qweb_exception.html">and evaluating the following expression: <code t-esc="qweb_exception.html"/></t>
|
||||
</p>
|
||||
<t t-if="'node' in qweb_exception.qweb">
|
||||
<pre id="exception_node" t-esc="qweb_exception.pretty_xml()"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -944,62 +941,74 @@
|
||||
</template>
|
||||
|
||||
<template id="500">
|
||||
<!-- This template should not use any variable except those provided by website.ir_http._handle_exception -->
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<!-- This template should not use any variable except those provided by website.ir_http._handle_exception -->
|
||||
<!-- no request.crsf_token, no theme style, no assets, ... cursor can be broken during rendering ! -->
|
||||
<!-- see test_05_reset_specific_view_controller_broken_request -->
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<html>
|
||||
<head>
|
||||
<title t-esc="status_message">Internal Server Error</title>
|
||||
<t t-set="debug" t-value="True"/>
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-js="false"/>
|
||||
<t t-call-assets="web.assets_common" t-css="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-css="false"/>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var button = $('#reset_templates_button');
|
||||
button.click(function() {
|
||||
var dialog = $('#reset_template_confirmation').modal('show');
|
||||
var input = dialog.find('input[type="text"]').val('').focus();
|
||||
var dialog_form = dialog.find('form');
|
||||
dialog_form.submit(function() {
|
||||
if (input.val() == dialog.find('.confirm_word').text()) {
|
||||
dialog.modal('hide');
|
||||
button.prop('disabled', true).text('Working...');
|
||||
$('#reset_templates_form').trigger('submit');
|
||||
} else {
|
||||
input.val('').focus();
|
||||
}
|
||||
|
||||
<link rel="stylesheet" href="/web/static/lib/bootstrap/css/bootstrap.css"/>
|
||||
<script src="/web/static/lib/jquery/jquery.js" type="text/javascript"/>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/util.js"/>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/collapse.js"/>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/modal.js"/>
|
||||
|
||||
<t t-if='views'>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var button = $('#reset_templates_button');
|
||||
button.click(function() {
|
||||
var dialog = $('#reset_template_confirmation').modal('show');
|
||||
var input = dialog.find('input[type="text"]').val('').focus();
|
||||
var dialog_form = dialog.find('form');
|
||||
dialog_form.submit(function() {
|
||||
if (input.val() == dialog.find('.confirm_word').text()) {
|
||||
dialog.modal('hide');
|
||||
button.prop('disabled', true).text('Working...');
|
||||
if ($("input[name='templates']:checked").length) {
|
||||
$('#reset_templates_form').attr('action', '/website/reset_templates');
|
||||
$('#reset_templates_form').trigger('submit');
|
||||
}
|
||||
} else {
|
||||
input.val('').focus();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
</t>
|
||||
</head>
|
||||
<body>
|
||||
<div role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true">
|
||||
<div t-if="views" role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true">
|
||||
<div class="modal-dialog">
|
||||
<form role="form">
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h3 class="modal-title">Reset templates</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<div class="form-group row mb0">
|
||||
<label for="page-name" class="col-md-9 col-form-label">
|
||||
<p>The selected templates will be reset to their factory settings.</p>
|
||||
<p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p>
|
||||
</label>
|
||||
<div class="col-md-3 mt16">
|
||||
<input type="text" class="form-control" required="required" placeholder="yes"/>
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h3 class="modal-title">Reset templates</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<div class="form-group row mb0">
|
||||
<label for="page-name" class="col-md-9 col-form-label">
|
||||
<p>The selected templates will be reset to their factory settings.</p>
|
||||
<p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p>
|
||||
</label>
|
||||
<div class="col-md-3 mt16">
|
||||
<input type="text" id="page-name" class="form-control" required="required" placeholder="yes"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<input type="submit" value="Confirm" class="btn btn-primary"/>
|
||||
<button type="button" class="btn" data-dismiss="modal" aria-label="Cancel">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<input type="submit" value="Confirm" class="btn btn-primary"/>
|
||||
<button type="button" class="btn" data-dismiss="modal" aria-label="Cancel">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1021,18 +1030,20 @@
|
||||
<div class="oe_structure">
|
||||
<h1 class="container mt32"><t t-esc="status_code"/>: <t t-esc="status_message"/></h1>
|
||||
</div>
|
||||
<div class="container" t-if="views">
|
||||
<div class="alert alert-danger" t-if="qweb_exception and editable" role="alert">
|
||||
<div class="container" t-if="views and editable">
|
||||
<div class="alert alert-danger" t-if="debug" role="alert">
|
||||
<h4>Template fallback</h4>
|
||||
<p>An error occured while rendering the template <code t-esc="qweb_exception.qweb['template']"/>.</p>
|
||||
<p>An error occured while rendering the template <code t-esc="qweb_exception.name"/>.</p>
|
||||
<p>If this error is caused by a change of yours in the templates, you have the possibility to reset one or more templates to their <strong>factory settings</strong>.</p>
|
||||
<form action="/website/reset_templates" method="post" id="reset_templates_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<form action="#" method="post" id="reset_templates_form">
|
||||
<ul class="oe_template_fallback">
|
||||
<li t-foreach="views" t-as="view">
|
||||
<label>
|
||||
<input type="checkbox" name="templates" t-att-value="view.id" t-att-checked="'checked' if view_first else None"/>
|
||||
<t t-esc="view.name"/>
|
||||
<span t-if="not view._get_original_view()" class="text-danger">
|
||||
(!!! This view will be removed )
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -251,7 +251,7 @@ actual arch.
|
||||
for view in self:
|
||||
arch_fs = None
|
||||
xml_id = view.xml_id or view.key
|
||||
if 'xml' in config['dev_mode'] and view.arch_fs and xml_id:
|
||||
if (self._context.get('read_arch_from_file') or 'xml' in config['dev_mode']) and view.arch_fs and xml_id:
|
||||
# It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
|
||||
fullpath = get_resource_path(*view.arch_fs.split('/'))
|
||||
if fullpath:
|
||||
|
||||
Reference in New Issue
Block a user