[IMP] website: improve reset view feature
- Store previous arch to be able to reset it (soft reset) - Add the possibility to reset from file if possible (hard reset) - Adapt frontend reset page to these new fields - `arch_fs` hack to check if view was modified got moved to new field `arch_updated` as we now need to keep track of the `arch_fs` to reset a broken view. Closes #32009 (task-1943001) This commit also: - adds the possibility to reset a view arch in its form view, including a diff viewer (github like). - adds string helper on those arch fields, as they are not documented, it is not always obvious to get the purpose of each fields. - adapts the font-size and headings to fit Odoo style on the error 500 page. The error page does not use Odoo's assets, just bootstrap.css (the cursor might be broken on the error 500 page so we can't use Odoo assets). - raise the caller on unexisting t-call during QWeb rendering. - adapts reset view code to single view as the code still had remains of its initial release (Odoo 9.0). Handling multiple views has no sense anymore. Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
This commit is contained in:
@@ -19,17 +19,17 @@ class TestWebsiteResetViews(odoo.tests.HttpCase):
|
||||
login="admin"
|
||||
)
|
||||
|
||||
def fix_it(self, page):
|
||||
def fix_it(self, page, mode='soft'):
|
||||
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.assertTrue('<button data-mode="soft" class="reset_templates_button' in resp.text)
|
||||
data = {'view_id': self.find_template(resp), 'redirect': page, 'mode': mode}
|
||||
resp = self.url_open('/website/reset_template', 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)
|
||||
find = re.search(r'<input.*type="hidden".*name="view_id".*value="([0-9]+)?"', response.text)
|
||||
return find and find.group(1)
|
||||
|
||||
def setUp(self):
|
||||
@@ -101,3 +101,18 @@ class TestWebsiteResetViews(odoo.tests.HttpCase):
|
||||
self.fix_it('/test_page_view')
|
||||
self.do_test('test_reset_page_view_complete_flow_part2')
|
||||
self.fix_it('/test_page_view')
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http')
|
||||
def test_08_reset_specific_page_view_hard_mode(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))
|
||||
# Break it again to have a previous arch different than file arch
|
||||
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")
|
||||
with self.assertRaises(AssertionError):
|
||||
# soft reset should not be able to reset the view as previous
|
||||
# version is also broken
|
||||
self.fix_it('/test_page_view')
|
||||
self.fix_it('/test_page_view', 'hard')
|
||||
|
||||
@@ -283,36 +283,17 @@ 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, 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.
|
||||
@http.route('/website/reset_template', type='http', auth='user', methods=['POST'], website=True, csrf=False)
|
||||
def reset_template(self, view_id, mode='soft', redirect='/', **kwargs):
|
||||
""" This method will try to reset a broken view.
|
||||
Given the mode, the view can either be:
|
||||
- Soft reset: restore to previous architeture.
|
||||
- Hard reset: it will read the original `arch` from the XML file if the
|
||||
view comes from an XML file (arch_fs).
|
||||
"""
|
||||
templates = request.httprequest.form.getlist('templates')
|
||||
for temp_id in templates:
|
||||
view = request.env['ir.ui.view'].browse(int(temp_id))
|
||||
if 'oe_structure' in view.key:
|
||||
# Particular xpathing view created in edit mode
|
||||
view.unlink()
|
||||
continue
|
||||
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
|
||||
})
|
||||
|
||||
view = request.env['ir.ui.view'].browse(int(view_id))
|
||||
# Deactivate COW to not fix a generic view by creating a specific
|
||||
view.with_context(website_id=None).reset_arch(mode)
|
||||
return request.redirect(redirect)
|
||||
|
||||
@http.route(['/website/publish'], type='json', auth="public", website=True)
|
||||
|
||||
@@ -250,41 +250,29 @@ class Http(models.AbstractModel):
|
||||
if code == 500:
|
||||
logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
|
||||
View = env["ir.ui.view"]
|
||||
values['views'] = View
|
||||
values['view'] = 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)
|
||||
try:
|
||||
# exception.name might be int, string
|
||||
exception_template = int(exception.name)
|
||||
except:
|
||||
exception_template = exception.name
|
||||
view = View._view_obj(exception_template)
|
||||
if exception.html and exception.html in view.arch:
|
||||
values['view'] = view
|
||||
else:
|
||||
try:
|
||||
# exception.name might be int, string
|
||||
exception_template = int(exception.name)
|
||||
except:
|
||||
exception_template = exception.name
|
||||
view = View._view_obj(exception_template)
|
||||
if exception.html in view.arch:
|
||||
values['views'] = view
|
||||
else:
|
||||
# There might be 2 cases where the exception code can't be found
|
||||
# in the view, either the error is in a child view or the code
|
||||
# contains branding (<div t-att-data="request.browse('ok')"/>).
|
||||
et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch'])
|
||||
node = et.xpath(exception.path)
|
||||
line = node is not None and etree.tostring(node[0], encoding='unicode')
|
||||
if line:
|
||||
values['views'] = View._views_get(exception_template).filtered(
|
||||
lambda v: line in v.arch
|
||||
)
|
||||
# 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
|
||||
)
|
||||
# There might be 2 cases where the exception code can't be found
|
||||
# in the view, either the error is in a child view or the code
|
||||
# contains branding (<div t-att-data="request.browse('ok')"/>).
|
||||
et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch'])
|
||||
node = et.xpath(exception.path)
|
||||
line = node is not None and etree.tostring(node[0], encoding='unicode')
|
||||
if line:
|
||||
values['view'] = View._views_get(exception_template).filtered(
|
||||
lambda v: line in v.arch
|
||||
)
|
||||
values['view'] = values['view'] and values['view'][0]
|
||||
|
||||
# Needed to show reset template on translated pages (`_prepare_qcontext` will set it for main lang)
|
||||
values['editable'] = request.uid and request.website.is_publisher()
|
||||
elif code == 403:
|
||||
|
||||
@@ -1027,12 +1027,18 @@
|
||||
<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"/>
|
||||
<style>
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<t t-if='views'>
|
||||
<t t-if='view'>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var button = $('#reset_templates_button');
|
||||
var button = $('.reset_templates_button');
|
||||
button.click(function() {
|
||||
$('#reset_templates_mode').val($(this).data('mode'));
|
||||
var dialog = $('#reset_template_confirmation').modal('show');
|
||||
var input = dialog.find('input[type="text"]').val('').focus();
|
||||
var dialog_form = dialog.find('form');
|
||||
@@ -1040,10 +1046,8 @@
|
||||
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');
|
||||
}
|
||||
$('#reset_templates_form').attr('action', '/website/reset_template');
|
||||
$('#reset_templates_form').trigger('submit');
|
||||
} else {
|
||||
input.val('').focus();
|
||||
}
|
||||
@@ -1056,18 +1060,22 @@
|
||||
</t>
|
||||
</head>
|
||||
<body>
|
||||
<div t-if="views" role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" t-ignore="true">
|
||||
<div t-if="view" 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>
|
||||
<h4 class="modal-title">Reset templates</h4>
|
||||
<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">
|
||||
<label for="page-name" class="col-md-12 col-form-label">
|
||||
<p>The selected templates will be reset to their factory settings.</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group row mb0">
|
||||
<label for="page-name" class="col-md-9 col-form-label">
|
||||
<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">
|
||||
@@ -1099,27 +1107,26 @@
|
||||
</header>
|
||||
<main>
|
||||
<div class="oe_structure">
|
||||
<h1 class="container mt32"><t t-esc="status_code"/>: <t t-esc="status_message"/></h1>
|
||||
<h2 class="container mt32"><t t-esc="status_code"/>: <t t-esc="status_message"/></h2>
|
||||
</div>
|
||||
<div class="container" t-if="views and editable">
|
||||
<div class="container" t-if="view 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.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>
|
||||
<p>If this error is caused by a change of yours in the templates, you have the possibility to reset the template to its <strong>factory settings</strong>.</p>
|
||||
<form action="#" method="post" id="reset_templates_form">
|
||||
<ul class="oe_template_fallback">
|
||||
<li t-foreach="views" t-as="view">
|
||||
<ul>
|
||||
<li>
|
||||
<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>
|
||||
<input type="hidden" name="redirect" t-att-value="request.httprequest.path"/>
|
||||
<button id="reset_templates_button">Reset selected templates</button>
|
||||
<input type="hidden" id="reset_templates_view_id" name="view_id" t-att-value="view.id"/>
|
||||
<input type="hidden" id="reset_templates_mode" name="mode"/>
|
||||
<button data-mode="soft" class="reset_templates_button btn btn-info">Restore previous version (soft reset).</button>
|
||||
<button t-if="view.arch_fs" data-mode="hard" class="reset_templates_button btn btn-outline-danger">Reset to initial version (hard reset).</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import uuid
|
||||
import itertools
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from functools import partial
|
||||
from difflib import HtmlDiff
|
||||
from operator import itemgetter
|
||||
|
||||
import json
|
||||
@@ -205,10 +206,18 @@ class View(models.Model):
|
||||
('kanban', 'Kanban'),
|
||||
('search', 'Search'),
|
||||
('qweb', 'QWeb')], string='View Type')
|
||||
arch = fields.Text(compute='_compute_arch', inverse='_inverse_arch', string='View Architecture', nodrop=True)
|
||||
arch_base = fields.Text(compute='_compute_arch_base', inverse='_inverse_arch_base', string='Base View Architecture')
|
||||
arch_db = fields.Text(string='Arch Blob', translate=xml_translate, oldname='arch')
|
||||
arch_fs = fields.Char(string='Arch Filename')
|
||||
arch = fields.Text(compute='_compute_arch', inverse='_inverse_arch', string='View Architecture',
|
||||
help="""This field should be used when accessing view arch. It will use translation.
|
||||
Note that it will read `arch_db` or `arch_fs` if in dev-xml mode.""")
|
||||
arch_base = fields.Text(compute='_compute_arch_base', inverse='_inverse_arch_base', string='Base View Architecture',
|
||||
help="This field is the same as `arch` field without translations")
|
||||
arch_db = fields.Text(string='Arch Blob', translate=xml_translate, oldname='arch',
|
||||
help="This field stores the view arch.")
|
||||
arch_fs = fields.Char(string='Arch Filename', help="""File from where the view originates.
|
||||
Useful to (hard) reset broken views or to read arch from file in dev-xml mode.""")
|
||||
arch_updated = fields.Boolean(string='Modified Architecture')
|
||||
arch_prev = fields.Text(string='Previous View Architecture', help="""This field will save the current `arch_db` before writing on it.
|
||||
Useful to (soft) reset a broken view.""")
|
||||
inherit_id = fields.Many2one('ir.ui.view', string='Inherited View', ondelete='restrict', index=True)
|
||||
inherit_children_ids = fields.One2many('ir.ui.view', 'inherit_id', string='Views which inherit from this one')
|
||||
field_parent = fields.Char(string='Child Field')
|
||||
@@ -238,7 +247,7 @@ actual arch.
|
||||
* if False, the view currently does not extend its parent but can be enabled
|
||||
""")
|
||||
|
||||
@api.depends('arch_db', 'arch_fs')
|
||||
@api.depends('arch_db', 'arch_fs', 'arch_updated')
|
||||
def _compute_arch(self):
|
||||
def resolve_external_ids(arch_fs, view_xml_id):
|
||||
def replacer(m):
|
||||
@@ -251,7 +260,9 @@ actual arch.
|
||||
for view in self:
|
||||
arch_fs = None
|
||||
xml_id = view.xml_id or view.key
|
||||
if (self._context.get('read_arch_from_file') or 'xml' in config['dev_mode']) and view.arch_fs and xml_id:
|
||||
read_file = self._context.get('read_arch_from_file') or \
|
||||
('xml' in config['dev_mode'] and not view.arch_updated)
|
||||
if read_file 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:
|
||||
@@ -272,6 +283,7 @@ actual arch.
|
||||
path_info = get_resource_from_path(self._context['install_filename'])
|
||||
if path_info:
|
||||
data['arch_fs'] = '/'.join(path_info[0:2])
|
||||
data['arch_updated'] = False
|
||||
view.write(data)
|
||||
|
||||
@api.depends('arch')
|
||||
@@ -284,6 +296,18 @@ actual arch.
|
||||
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
|
||||
view_wo_lang.arch = view.arch_base
|
||||
|
||||
@api.multi
|
||||
def reset_arch(self, mode='soft'):
|
||||
for view in self:
|
||||
arch = False
|
||||
if mode == 'soft':
|
||||
arch = view.arch_prev
|
||||
elif mode == 'hard' and view.arch_fs:
|
||||
arch = view.with_context(read_arch_from_file=True).arch
|
||||
if arch:
|
||||
# Don't save current arch in previous since we reset, this arch is probably broken
|
||||
view.with_context(no_save_prev=True).write({'arch_db': arch})
|
||||
|
||||
@api.depends('write_date')
|
||||
def _compute_model_data_id(self):
|
||||
# get the first ir_model_data record corresponding to self
|
||||
@@ -422,6 +446,8 @@ actual arch.
|
||||
values['key'] = "%s.gen_key_%s" % (values.get('model'), str(uuid.uuid4())[:6])
|
||||
if not values.get('name'):
|
||||
values['name'] = "%s %s" % (values.get('model'), values['type'])
|
||||
# Create might be called with either `arch` (xml files), `arch_base` (form view) or `arch_db`.
|
||||
values['arch_prev'] = values.get('arch_base') or values.get('arch_db') or values.get('arch')
|
||||
values.update(self._compute_defaults(values))
|
||||
|
||||
self.clear_caches()
|
||||
@@ -429,10 +455,10 @@ actual arch.
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# If view is modified we remove the arch_fs information thus activating the arch_db
|
||||
# version. An `init` of the view will restore the arch_fs for the --dev mode
|
||||
# Keep track if view was modified. That will be useful for the --dev mode
|
||||
# to prefer modified arch over file arch.
|
||||
if ('arch' in vals or 'arch_base' in vals) and 'install_filename' not in self._context:
|
||||
vals['arch_fs'] = False
|
||||
vals['arch_updated'] = True
|
||||
|
||||
# drop the corresponding view customizations (used for dashboards for example), otherwise
|
||||
# not all users would see the updated views
|
||||
@@ -441,6 +467,8 @@ actual arch.
|
||||
custom_view.unlink()
|
||||
|
||||
self.clear_caches()
|
||||
if 'arch_db' in vals and not self.env.context.get('no_save_prev'):
|
||||
vals['arch_prev'] = self.arch_db
|
||||
return super(View, self).write(self._compute_defaults(vals))
|
||||
|
||||
def unlink(self):
|
||||
@@ -1448,3 +1476,78 @@ actual arch.
|
||||
view._check_xml()
|
||||
except Exception as e:
|
||||
self.raise_view_error("Can't validate view:\n%s" % e, view.id)
|
||||
|
||||
|
||||
class ResetViewArchWizard(models.TransientModel):
|
||||
""" A wizard to 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_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)
|
||||
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.")
|
||||
|
||||
@api.one
|
||||
@api.depends('reset_mode', '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
|
||||
|
||||
soft = self.reset_mode == 'soft'
|
||||
arch_to_compare = False
|
||||
if soft:
|
||||
arch_to_compare = self.view_id.arch_prev
|
||||
elif not soft and self.view_id.arch_fs:
|
||||
arch_to_compare = self.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(),
|
||||
self.view_id.arch.splitlines(),
|
||||
_("Previous Arch") if soft else _("File Arch"),
|
||||
_("Current Arch"),
|
||||
context=True, # Show only diff lines, not all the code
|
||||
)
|
||||
diff = handle_style(diff)
|
||||
self.arch_diff = diff
|
||||
|
||||
@api.multi
|
||||
def reset_view_button(self):
|
||||
self.ensure_one()
|
||||
self.view_id.reset_arch(self.reset_mode)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -358,7 +358,9 @@ class QWeb(object):
|
||||
except QWebException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise QWebException("load could not load template", name=template)
|
||||
template = options.get('caller_template', template)
|
||||
path = options['last_path_node']
|
||||
raise QWebException("load could not load template", e, path, name=template)
|
||||
|
||||
if document is None:
|
||||
raise QWebException("Template not found", name=template)
|
||||
@@ -1454,23 +1456,23 @@ class QWeb(object):
|
||||
)
|
||||
)
|
||||
|
||||
if nsmap or call_options:
|
||||
# copy the original dict of options to pass to the callee
|
||||
name_options = self._make_name('options')
|
||||
content.append(
|
||||
# options_ = options.copy()
|
||||
ast.Assign(
|
||||
targets=[ast.Name(id=name_options, ctx=ast.Store())],
|
||||
value=ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='options', ctx=ast.Load()),
|
||||
attr='copy',
|
||||
ctx=ast.Load()
|
||||
),
|
||||
args=[], keywords=[], starargs=None, kwargs=None
|
||||
)
|
||||
name_options = self._make_name('options')
|
||||
# copy the original dict of options to pass to the callee
|
||||
content.append(
|
||||
# options_ = options.copy()
|
||||
ast.Assign(
|
||||
targets=[ast.Name(id=name_options, ctx=ast.Store())],
|
||||
value=ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='options', ctx=ast.Load()),
|
||||
attr='copy',
|
||||
ctx=ast.Load()
|
||||
),
|
||||
args=[], keywords=[], starargs=None, kwargs=None
|
||||
)
|
||||
)
|
||||
)
|
||||
if nsmap or call_options:
|
||||
|
||||
if call_options:
|
||||
# update this dict with the content of `t-call-options`
|
||||
@@ -1568,8 +1570,30 @@ class QWeb(object):
|
||||
keywords=[], starargs=None, kwargs=None
|
||||
))
|
||||
)
|
||||
else:
|
||||
name_options = 'options'
|
||||
|
||||
# options_.update({
|
||||
# 'caller_template': str(options.get('template')),
|
||||
# 'last_path_node': str(options['root'].getpath(el)),
|
||||
# })
|
||||
content.append(
|
||||
ast.Expr(ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id=name_options, ctx=ast.Load()),
|
||||
attr='update',
|
||||
ctx=ast.Load()
|
||||
),
|
||||
args=[
|
||||
ast.Dict(
|
||||
keys=[ast.Str(s='caller_template'), ast.Str(s='last_path_node')],
|
||||
values=[
|
||||
ast.Str(s=str(options.get('template'))),
|
||||
ast.Str(s=str(options['root'].getpath(el))),
|
||||
]
|
||||
)
|
||||
],
|
||||
keywords=[], starargs=None, kwargs=None
|
||||
))
|
||||
)
|
||||
|
||||
# self.compile($tmpl, options)(self, append, values_copy)
|
||||
content.append(
|
||||
|
||||
@@ -95,6 +95,39 @@
|
||||
</record>
|
||||
<menuitem action="action_ui_view" id="menu_action_ui_view" parent="base.next_id_2" sequence="2"/>
|
||||
|
||||
<!-- Reset view wizard -->
|
||||
<record id="reset_view_arch_wizard_view" model="ir.ui.view">
|
||||
<field name="name">Reset View Architecture</field>
|
||||
<field name="model">reset.view.arch.wizard</field>
|
||||
<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="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>
|
||||
<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="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<act_window id="reset_view_arch_wizard_action"
|
||||
name="Reset View Architecture"
|
||||
src_model="ir.ui.view"
|
||||
res_model="reset.view.arch.wizard"
|
||||
view_type="form"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
groups="base.group_erp_manager"/>
|
||||
|
||||
<!-- View customizations -->
|
||||
<record id="view_view_custom_form" model="ir.ui.view">
|
||||
<field name="model">ir.ui.view.custom</field>
|
||||
|
||||
Reference in New Issue
Block a user