[IMP] *: remove _render from ir.ui.view and simplify report

There were inconsistencies in the calls to `_render`.
* the view context could contain information that misled developers.
Indeed, the context and value of the view are not supposed to be found
in the rendering. Thus by calling `ir.qweb` with the name of the
template, we ensure that there is no unwanted information and in
addition the cache key is that of the name of the template which saves
a query.
* the context used for rendering was modified by a method on
`ir.ui.view`, except this is not information used by this model. There
is now a `_prepare_environment` method residing on `ir.qweb`. This
method allows to modify the value dictionary as well as the context in
which the rendering will be done. This preparation of the data as well
as my security check is done only once per rendering. This also saves
some queries
* Freeze options for rendering were inconsistent. It could be that
options on which rendering depends were not part of the cache key. Thus,
depending on the user who generated the generation of the rendering
function, there was or was not information in the template. For example
for automatic branding. This is no longer possible, because it is the
context that is used. The options serving as a cache key are only
recorded for information (for the profiling system for example). A
simplification of the `ir.qweb.field` models could be made.

The report rendering and call `ir.qweb` instead of `ir.ui.view`.

Part-of: odoo/odoo#85110
This commit is contained in:
Gorash
2022-03-29 10:56:15 +02:00
committed by Martin Trigaux
parent a327b2ec8a
commit 880954ebfc
85 changed files with 797 additions and 748 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ class OnboardingController(http.Controller):
return {}
return {
'html': request.env.ref('account.account_invoice_onboarding_panel')._render({
'html': request.env['ir.qweb']._render('account.account_invoice_onboarding_panel', {
'company': company,
'state': company.get_and_update_account_invoice_onboarding_state()
})
@@ -34,7 +34,7 @@ class OnboardingController(http.Controller):
return {}
return {
'html': request.env.ref('account.account_dashboard_onboarding_panel')._render({
'html': request.env['ir.qweb']._render('account.account_dashboard_onboarding_panel', {
'company': company,
'state': company.get_and_update_account_dashboard_onboarding_state()
})
+5 -5
View File
@@ -200,12 +200,12 @@ class ResCompany(models.Model):
@api.depends('terms_type')
def _compute_invoice_terms_html(self):
term_template = self.env.ref("account.account_default_terms_and_conditions", False)
if not term_template:
return
for company in self.filtered(lambda company: is_html_empty(company.invoice_terms_html) and company.terms_type == 'html'):
company.invoice_terms_html = term_template._render({'company_name': company.name, 'company_country': company.country_id.name}, engine='ir.qweb')
html = self.env['ir.qweb']._render('account.account_default_terms_and_conditions',
{'company_name': company.name, 'company_country': company.country_id.name},
raise_if_not_found=False)
if html:
company.invoice_terms_html = html
def get_and_update_account_invoice_onboarding_state(self):
""" This method is called on the controller rendering method and ensures that the animations
@@ -35,7 +35,7 @@ class AccountTourUploadBill(models.TransientModel):
self.env.company.country_id.name,
] if x]
ref = 'INV/%s/0001' % invoice_date.strftime('%Y/%m')
html = self.env.ref('account.bill_preview')._render({
html = self.env['ir.qweb']._render('account.bill_preview', {
'company_name': self.env.company.name,
'company_street_address': addr,
'invoice_name': 'Invoice ' + ref,
@@ -42,12 +42,13 @@ class AccountEdiFormat(models.Model):
pdf_writer.convert_to_pdfa()
except Exception as e:
_logger.exception("Error while converting to PDF/A: %s", e)
metadata_template = self.env.ref('account_edi_facturx.account_invoice_pdfa_3_facturx_metadata', raise_if_not_found=False)
if metadata_template:
pdf_writer.add_file_metadata(metadata_template._render({
html = self.env['ir.qweb']._render('account_edi_facturx.account_invoice_pdfa_3_facturx_metadata', {
'title': edi_document.move_id.name,
'date': fields.Date.context_today(self),
}).encode())
},
raise_if_not_found=False)
if html:
pdf_writer.add_file_metadata(html.encode())
def _export_facturx(self, invoice):
@@ -71,7 +72,7 @@ class AccountEdiFormat(models.Model):
}
xml_content = markupsafe.Markup("<?xml version='1.0' encoding='UTF-8'?>")
xml_content += self.env.ref('account_edi_facturx.account_invoice_facturx_export')._render(template_values)
xml_content += self.env['ir.qweb']._render('account_edi_facturx.account_invoice_facturx_export', template_values)
return self.env['ir.attachment'].create({
'name': 'factur-x.xml',
'raw': xml_content.encode(),
@@ -207,7 +207,7 @@ class AccountEdiFormat(models.Model):
self.ensure_one()
# Create file content.
xml_content = markupsafe.Markup("<?xml version='1.0' encoding='UTF-8'?>")
xml_content += self.env.ref('account_edi_ubl.export_ubl_invoice')._render(self._get_ubl_values(invoice))
xml_content += self.env['ir.qweb']._render('account_edi_ubl.export_ubl_invoice', self._get_ubl_values(invoice))
xml_name = '%s_ubl_2_1.xml' % (invoice.name.replace('/', '_'))
return self.env['ir.attachment'].create({
'name': xml_name,
+2 -2
View File
@@ -48,7 +48,7 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
leads.flush()
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=1289): # 1281-1289 generally - crm only: 1204
with self.assertQueryCount(user_sales_manager=1283): # 1277-1283 generally - crm only: 1204
self.env['crm.team'].browse(self.sales_teams.ids)._action_assign_leads(work_days=2)
# teams assign
@@ -174,7 +174,7 @@ class TestLeadAssignPerf(TestLeadAssignCommon):
leads.flush()
with self.with_user('user_sales_manager'):
with self.assertQueryCount(user_sales_manager=6547): # 6539-6546 generally
with self.assertQueryCount(user_sales_manager=6502): # 6494-6502 generally
self.env['crm.team'].browse(sales_teams.ids)._action_assign_leads(work_days=30)
# teams assign
+1 -2
View File
@@ -846,8 +846,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
# TODO: Make api.multi
def _send_expense_success_mail(self, msg_dict, expense):
mail_template_id = 'hr_expense.hr_expense_template_register' if expense.employee_id.user_id else 'hr_expense.hr_expense_template_register_no_user'
expense_template = self.env.ref(mail_template_id)
rendered_body = expense_template._render({'expense': expense}, engine='ir.qweb')
rendered_body = self.env['ir.qweb']._render(mail_template_id, {'expense': expense})
body = self.env['mail.render.mixin']._replace_local_links(rendered_body)
# TDE TODO: seems louche, check to use notify
if expense.employee_id.user_id.partner_id:
@@ -31,7 +31,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
self.richard_emp.generate_work_entries(date(2018, 1, 1), date(2018, 1, 2))
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
with self.assertQueryCount(__system__=92, admin=93):
with self.assertQueryCount(__system__=91, admin=92):
leave.action_validate()
leave.action_refuse()
+1 -1
View File
@@ -2,4 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_http
from . import ir_ui_view
from . import ir_qweb
+52
View File
@@ -0,0 +1,52 @@
# -*- coding: ascii -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import fnmatch
import werkzeug
from odoo import models
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import slug, unslug_url, url_for
def keep_query(*keep_params, **additional_params):
"""
Generate a query string keeping the current request querystring's parameters specified
in ``keep_params`` and also adds the parameters specified in ``additional_params``.
Multiple values query string params will be merged into a single one with comma seperated
values.
The ``keep_params`` arguments can use wildcards too, eg:
keep_query('search', 'shop_*', page=4)
"""
if not keep_params and not additional_params:
keep_params = ('*',)
params = additional_params.copy()
qs_keys = list(request.httprequest.args) if request else []
for keep_param in keep_params:
for param in fnmatch.filter(qs_keys, keep_param):
if param not in additional_params and param in qs_keys:
params[param] = request.httprequest.args.getlist(param)
return werkzeug.urls.url_encode(params)
class IrQweb(models.AbstractModel):
_inherit = "ir.qweb"
def _prepare_environment(self, values):
irQweb = super()._prepare_environment(values)
values['slug'] = slug
values['unslug_url'] = unslug_url
values['keep_query'] = keep_query
if (not irQweb.env.context.get('minimal_qcontext') and
request and request.is_frontend):
return irQweb._prepare_frontend_environment(values)
return irQweb
def _prepare_frontend_environment(self, values):
values['url_for'] = url_for
return self
-16
View File
@@ -1,16 +0,0 @@
# -*- coding: ascii -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.addons.http_routing.models.ir_http import slug, unslug_url
class IrUiView(models.Model):
_inherit = ["ir.ui.view"]
@api.model
def _prepare_qcontext(self):
qcontext = super(IrUiView, self)._prepare_qcontext()
qcontext['slug'] = slug
qcontext['unslug_url'] = unslug_url
return qcontext
@@ -58,14 +58,13 @@ class ImLivechatChannel(models.Model):
channel.are_you_inside = bool(self.env.uid in [u.id for u in channel.user_ids])
def _compute_script_external(self):
view = self.env.ref('im_livechat.external_loader')
values = {
"dbname": self._cr.dbname,
}
for record in self:
values["channel_id"] = record.id
values["url"] = record.get_base_url()
record.script_external = view._render(values) if record.id else False
record.script_external = self.env['ir.qweb']._render('im_livechat.external_loader', values) if record.id else False
def _compute_web_page_link(self):
for record in self:
+1 -2
View File
@@ -163,8 +163,7 @@ class MailChannel(models.Model):
"company": company,
"channel": self,
}
template = self.env.ref('im_livechat.livechat_email_template')
mail_body = template._render(render_context, engine='ir.qweb', minimal_qcontext=True)
mail_body = self.env['ir.qweb']._render('im_livechat.livechat_email_template', render_context, minimal_qcontext=True)
mail_body = self.env['mail.render.mixin']._replace_local_links(mail_body)
mail = self.env['mail.mail'].sudo().create({
'subject': _('Conversation with %s', self.livechat_operator_id.user_livechat_username or self.livechat_operator_id.name),
@@ -23,7 +23,7 @@ class AccountEdiFormat(models.Model):
self.ensure_one()
# Create file content.
xml_content = markupsafe.Markup("<?xml version='1.0' encoding='UTF-8'?>")
xml_content += self.env.ref('l10n_be_edi.export_efff_invoice')._render(self._get_efff_values(invoice))
xml_content += self.env['ir.qweb']._render('l10n_be_edi.export_efff_invoice', self._get_efff_values(invoice))
xml_name = '%s.xml' % invoice._get_efff_name()
return self.env['ir.attachment'].create({
'name': xml_name,
+1 -1
View File
@@ -206,7 +206,7 @@ class AccountMove(models.Model):
:return: The XML content as str.
'''
template_values = self._prepare_fatturapa_export_values()
content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export')._render(template_values)
content = self.env['ir.qweb']._render('l10n_it_edi.account_invoice_it_FatturaPA_export', template_values)
return content
def _post(self, soft=True):
@@ -67,7 +67,7 @@ class AccountEdiFormat(models.Model):
self.ensure_one()
# Create file content.
xml_content = markupsafe.Markup("<?xml version='1.0' encoding='UTF-8'?>")
xml_content += self.env.ref('l10n_nl_edi.export_nlcius_invoice')._render(self._get_nlcius_values(invoice))
xml_content += self.env['ir.qweb']._render('l10n_nl_edi.export_nlcius_invoice', self._get_nlcius_values(invoice))
vat = invoice.company_id.partner_id.commercial_partner_id.vat
xml_name = 'nlcius-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_'))
return self.env['ir.attachment'].create({
@@ -52,7 +52,7 @@ class AccountEdiFormat(models.Model):
def _export_ehf_3(self, invoice):
self.ensure_one()
# Create file content.
xml_content = self.env.ref('l10n_no_edi.export_ehf_3_invoice')._render(self._get_ehf_3_values(invoice))
xml_content = self.env['ir.qweb']._render('l10n_no_edi.export_ehf_3_invoice', self._get_ehf_3_values(invoice))
vat = invoice.company_id.partner_id.commercial_partner_id.vat
xml_name = 'ehf-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_'))
return self.env['ir.attachment'].create({
+6 -10
View File
@@ -436,25 +436,23 @@ class MailActivity(models.Model):
def action_notify(self):
if not self:
return
original_context = self.env.context
body_template = self.env.ref('mail.message_activity_assigned')
for activity in self:
if activity.user_id.lang:
# Send the notification in the assigned user's language
self = self.with_context(lang=activity.user_id.lang)
body_template = body_template.with_context(lang=activity.user_id.lang)
activity = activity.with_context(lang=activity.user_id.lang)
model_description = self.env['ir.model']._get(activity.res_model).display_name
body = body_template._render(
model_description = activity.env['ir.model']._get(activity.res_model).display_name
body = activity.env['ir.qweb']._render(
'mail.message_activity_assigned',
dict(
activity=activity,
model_description=model_description,
access_link=self.env['mail.thread']._notify_get_action_link('view', model=activity.res_model, res_id=activity.res_id),
access_link=activity.env['mail.thread']._notify_get_action_link('view', model=activity.res_model, res_id=activity.res_id),
),
engine='ir.qweb',
minimal_qcontext=True
)
record = self.env[activity.res_model].browse(activity.res_id)
record = activity.env[activity.res_model].browse(activity.res_id)
if activity.user_id:
record.message_notify(
partner_ids=activity.user_id.partner_id.ids,
@@ -466,8 +464,6 @@ class MailActivity(models.Model):
model_description=model_description,
email_layout_xmlid='mail.mail_notification_light',
)
body_template = body_template.with_context(original_context)
self = self.with_context(original_context)
def action_done(self):
""" Wrapper without feedback because web button add context as
+2 -7
View File
@@ -435,17 +435,12 @@ class MailActivityMixin(models.AbstractModel):
if self.env.context.get('mail_activity_automation_skip'):
return False
view_ref = views_or_xmlid.id if isinstance(views_or_xmlid, models.BaseModel) else views_or_xmlid
render_context = render_context or dict()
if isinstance(views_or_xmlid, str):
views = self.env.ref(views_or_xmlid, raise_if_not_found=False)
else:
views = views_or_xmlid
if not views:
return
activities = self.env['mail.activity']
for record in self:
render_context['object'] = record
note = views._render(render_context, engine='ir.qweb', minimal_qcontext=True)
note = self.env['ir.qweb']._render(view_ref, render_context, minimal_qcontext=True, raise_if_not_found=False)
activities |= record.activity_schedule(act_type_xmlid=act_type_xmlid, date_deadline=date_deadline, summary=summary, note=note, **act_values)
return activities
+2 -3
View File
@@ -263,8 +263,7 @@ Kind Regards,</p>"""
body = self.alias_bounced_content
else:
body = self._get_alias_bounced_body_fallback(message_dict)
template = self.env.ref('mail.mail_bounce_alias_security', raise_if_not_found=True)
return template._render({
return self.env['ir.qweb']._render('mail.mail_bounce_alias_security', {
'body': body,
'message': message_dict
}, engine='ir.qweb', minimal_qcontext=True)
}, minimal_qcontext=True)
+21 -32
View File
@@ -175,25 +175,20 @@ class MailRenderMixin(models.AbstractModel):
@api.model
def _render_encapsulate(self, layout_xmlid, html, add_context=None, context_record=None):
try:
template = self.env.ref(layout_xmlid, raise_if_not_found=True)
except ValueError:
_logger.warning('QWeb template %s not found when rendering encapsulation template.' % (layout_xmlid))
else:
record_name = context_record.display_name if context_record else ''
model_description = self.env['ir.model']._get(context_record._name).display_name if context_record else False
template_ctx = {
'body': html,
'record_name': record_name,
'model_description': model_description,
'company': context_record['company_id'] if (context_record and 'company_id' in context_record) else self.env.company,
'record': context_record,
}
if add_context:
template_ctx.update(**add_context)
template_ctx = {
'body': html,
'record_name': context_record.display_name if context_record else '',
'model_description': self.env['ir.model']._get(context_record._name).display_name if context_record else False,
'company': context_record['company_id'] if (context_record and 'company_id' in context_record) else self.env.company,
'record': context_record,
}
if add_context:
template_ctx.update(**add_context)
html = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
html = self.env['mail.render.mixin']._replace_local_links(html)
html = self.env['ir.qweb']._render(layout_xmlid, template_ctx, minimal_qcontext=True, raise_if_not_found=False)
if not html:
_logger.warning('QWeb template %s not found when rendering encapsulation template.' % (layout_xmlid))
html = self.env['mail.render.mixin']._replace_local_links(html)
return html
@api.model
@@ -289,13 +284,13 @@ class MailRenderMixin(models.AbstractModel):
group = self.env.ref('mail.group_mail_template_editor')
raise AccessError(_('Only users belonging to the "%s" group can modify dynamic templates.', group.name)) from e
_logger.info("Failed to render template : %s", template_src, exc_info=True)
raise UserError(_("Failed to render QWeb template : %s)", e)) from e
raise UserError(_("Failed to render QWeb template : %s)", template_src)) from e
results[record.id] = render_result
return results
@api.model
def _render_template_qweb_view(self, template_src, model, res_ids,
def _render_template_qweb_view(self, view_xmlid, model, res_ids,
add_context=None, options=None):
""" Render a QWeb template based on an ir.ui.view content.
@@ -303,7 +298,7 @@ class MailRenderMixin(models.AbstractModel):
variables are added:
* ``object``: record based on which the template is rendered;
:param str template_src: source QWeb template. It should be a string
:param str view_xmlid: source QWeb template. It should be a string
XmlID allowing to fetch an ``ir.ui.view``;
:param str model: see ``MailRenderMixin._render_template()``;
:param list res_ids: see ``MailRenderMixin._render_template()``;
@@ -319,27 +314,21 @@ class MailRenderMixin(models.AbstractModel):
if any(r is None for r in res_ids):
raise ValueError(_('Template rendering should be called on a valid record IDs.'))
view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view']
results = dict.fromkeys(res_ids, u"")
if not view:
return results
results = {}
# prepare template variables
variables = self._render_eval_context()
if add_context:
variables.update(**add_context)
safe_eval.check_values(variables)
for record in self.env[model].browse(res_ids):
variables['object'] = record
try:
render_result = view._render(variables, engine='ir.qweb', minimal_qcontext=True, options=options)
render_result = self.env['ir.qweb']._render(view_xmlid, variables, minimal_qcontext=True, raise_if_not_found=False, **(options or {}))
results[record.id] = render_result
except Exception as e:
_logger.info("Failed to render template : %s (%d)", template_src, view.id, exc_info=True)
raise UserError(_("Failed to render template : %(xml_id)s (%(view_id)d)",
xml_id=template_src,
view_id=view.id))
results[record.id] = render_result
_logger.info("Failed to render template : %s", view_xmlid, exc_info=True)
raise UserError(_("Failed to render template : %s") % view_xmlid)
return results
+27 -29
View File
@@ -284,42 +284,40 @@ class MailTemplate(models.Model):
values.pop('email_from')
# encapsulate body
if email_layout_xmlid and values['body_html']:
try:
template = self.env.ref(email_layout_xmlid, raise_if_not_found=True)
except ValueError:
record = self.env[self.model].browse(res_id)
model = self.env['ir.model']._get(record._name)
if self.lang:
lang = self._render_lang([res_id])[res_id]
model = model.with_context(lang=lang)
template_ctx = {
# message
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
'subtype': self.env['mail.message.subtype'].sudo(),
# record
'model_description': model.display_name,
'record': record,
'record_name': False,
'subtitle': False,
# user / environment
'company': 'company_id' in record and record['company_id'] or self.env.company,
'email_add_signature': False,
'signature': '',
'website_url': '',
# tools
'is_html_empty': is_html_empty,
}
body = model.env['ir.qweb']._render(email_layout_xmlid, template_ctx, minimal_qcontext=True, raise_if_not_found=False)
if not body:
_logger.warning(
'QWeb template %s not found when sending template %s. Sending without layout.',
email_layout_xmlid,
self.name
)
else:
record = self.env[self.model].browse(res_id)
model = self.env['ir.model']._get(record._name)
if self.lang:
lang = self._render_lang([res_id])[res_id]
template = template.with_context(lang=lang)
model = model.with_context(lang=lang)
values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
template_ctx = {
# message
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
'subtype': self.env['mail.message.subtype'].sudo(),
# record
'model_description': model.display_name,
'record': record,
'record_name': False,
'subtitle': False,
# user / environment
'company': 'company_id' in record and record['company_id'] or self.env.company,
'email_add_signature': False,
'signature': '',
'website_url': '',
# tools
'is_html_empty': is_html_empty,
}
body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
mail = self.env['mail.mail'].sudo().create(values)
# manage attachments
+10 -21
View File
@@ -937,9 +937,9 @@ class MailThread(models.AbstractModel):
# check it does not directly contact catchall
if catchall_alias and email_to_localparts and all(email_localpart == catchall_alias for email_localpart in email_to_localparts):
_logger.info('Routing mail from %s to %s with Message-Id %s: direct write to catchall, bounce', email_from, email_to, message_id)
body = self.env.ref('mail.mail_bounce_catchall')._render({
body = self.env['ir.qweb']._render('mail.mail_bounce_catchall', {
'message': message,
}, engine='ir.qweb')
})
self._routing_create_bounce_email(email_from, body, message, references=message_id, reply_to=self.env.company.email)
return []
@@ -1886,15 +1886,12 @@ class MailThread(models.AbstractModel):
values['slug'] = slug
except ImportError:
values['slug'] = lambda self: self.id
if isinstance(views_or_xmlid, str):
views = self.env.ref(views_or_xmlid, raise_if_not_found=False)
else:
views = views_or_xmlid
if not views:
return
view_ref = views_or_xmlid.id if isinstance(views_or_xmlid, models.BaseModel) else views_or_xmlid
for record in self:
values['object'] = record
rendered_template = views._render(values, engine='ir.qweb', minimal_qcontext=True)
rendered_template = self.env['ir.qweb']._render(view_ref, values, minimal_qcontext=True, raise_if_not_found=False)
if not rendered_template:
continue
if message_log:
return record._message_log(body=rendered_template, **kwargs)
else:
@@ -2236,12 +2233,6 @@ class MailThread(models.AbstractModel):
email_layout_xmlid = msg_vals.get('email_layout_xmlid') if msg_vals else message.email_layout_xmlid
template_xmlid = email_layout_xmlid if email_layout_xmlid else 'mail.message_notification_email'
try:
base_template = self.env.ref(template_xmlid, raise_if_not_found=True).with_context(lang=template_values['lang']) # 1 query
except ValueError:
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
base_template = False
base_mail_values = self._notify_by_email_get_base_mail_values(message, additional_values={'auto_delete': mail_auto_delete})
# Clean the context to get rid of residual default_* keys that could cause issues during
@@ -2264,9 +2255,9 @@ class MailThread(models.AbstractModel):
# {company, is_discussion, lang, message, model_description, record, record_name, signature, subtype, tracking_values, website_url}
# {actions, button_access, has_button_access, recipients}
if base_template:
mail_body = base_template._render(render_values, engine='ir.qweb', minimal_qcontext=True)
else:
mail_body = self.env['ir.qweb']._render(template_xmlid, render_values, minimal_qcontext=True, raise_if_not_found=False, lang=template_values['lang'])
if not mail_body:
_logger.warning('QWeb template %s not found or is empty when sending notification emails. Sending without layouting.', template_xmlid)
mail_body = message.body
mail_body = self.env['mail.render.mixin']._replace_local_links(mail_body)
@@ -2806,8 +2797,6 @@ class MailThread(models.AbstractModel):
if not self.env.registry.ready: # Don't send notification during install
return
view = self.env['ir.ui.view'].browse(self.env['ir.model.data']._xmlid_to_res_id(template))
for record in self:
model_description = self.env['ir.model']._get(record._name).display_name
values = {
@@ -2815,7 +2804,7 @@ class MailThread(models.AbstractModel):
'model_description': model_description,
'access_link': record._notify_get_action_link('view'),
}
assignation_msg = view._render(values, engine='ir.qweb', minimal_qcontext=True)
assignation_msg = self.env['ir.qweb']._render(template, values, minimal_qcontext=True)
assignation_msg = self.env['mail.render.mixin']._replace_local_links(assignation_msg)
record.message_notify(
subject=_('You have been assigned to %s', record.display_name),
+1 -1
View File
@@ -201,7 +201,7 @@ class PortalMailGroup(http.Controller):
'msg_more_count': message_count - self._replies_per_page,
'replies_per_page': self._replies_per_page,
}
return request.env.ref('mail_group.messages_short')._render(values, engine='ir.qweb')
return request.env['ir.qweb']._render('mail_group.messages_short', values)
# ------------------------------------------------------------
# SUBSCRIPTION
+3 -4
View File
@@ -448,8 +448,7 @@ class MailGroup(models.Model):
'unsub_label': f'{base_url}/groups?unsubscribe',
'unsub_url': f'{base_url}/groups?unsubscribe&group_id={self.id}&token={access_token}&email={email_url_encoded}',
}
template = self.env.ref('mail_group.mail_group_footer')
footer = template._render(template_values, engine='ir.qweb', minimal_qcontext=True)
footer = self.env['ir.qweb']._render('mail_group.mail_group_footer', template_values, minimal_qcontext=True)
member_body = tools.append_content_to_html(body, footer, plaintext=False)
mail_values.append({
@@ -493,10 +492,10 @@ class MailGroup(models.Model):
moderators_to_notify = group.moderator_ids
MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
for moderator in moderators_to_notify:
body = template._render({
body = self.env['ir.qweb']._render('mail_group.mail_group_notify_moderation', {
'moderator': moderator,
'group': group,
}, engine='ir.qweb', minimal_qcontext=True)
}, minimal_qcontext=True)
email_from = moderator.company_id.catchall_formatted or moderator.company_id.email_formatted
MailThread.message_notify(
partner_ids=moderator.partner_id.ids,
+1 -2
View File
@@ -10,8 +10,7 @@ from odoo.tools import mute_logger, append_content_to_html
class TestMailGroup(TestMailListCommon):
def test_clean_email_body(self):
template = self.env.ref('mail_group.mail_group_footer')
footer = template._render({'group_url': 'Test remove footer'}, engine='ir.qweb', minimal_qcontext=True)
footer = self.env['ir.qweb']._render('mail_group.mail_group_footer', {'group_url': 'Test remove footer'}, minimal_qcontext=True)
body = append_content_to_html("<div>Test email body</div>", footer, plaintext=False)
result = self.env['mail.group']._clean_email_body(body)
@@ -41,11 +41,14 @@ class MailComposeMessage(models.TransientModel):
self.mass_mailing_id = mass_mailing.id
recipients_info = self._process_recipient_values(res)
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout', raise_if_not_found=False)
for res_id in res_ids:
mail_values = res[res_id]
if mail_values.get('body_html') and mass_mail_layout:
mail_values['body_html'] = mass_mail_layout._render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True)
if mail_values.get('body_html'):
body = self.env['ir.qweb']._render('mass_mailing.mass_mailing_mail_layout',
{'body': mail_values['body_html']},
minimal_qcontext=True, raise_if_not_found=False)
if body:
mail_values['body_html'] = body
trace_vals = {
'model': self.model,
@@ -31,8 +31,6 @@ class TestMassMailing(models.TransientModel):
invalid_candidates.append(candidate)
mailing = self.mass_mailing_id
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout')
record = self.env[mailing.mailing_model_real].search([], limit=1)
# If there is atleast 1 record for the model used in this mailing, then we use this one to render the template
@@ -56,7 +54,7 @@ class TestMassMailing(models.TransientModel):
'reply_to': mailing.reply_to,
'email_to': valid_email,
'subject': subject,
'body_html': mass_mail_layout._render({'body': full_body}, engine='ir.qweb', minimal_qcontext=True),
'body_html': self.env['ir.qweb']._render('mass_mailing.mass_mailing_mail_layout', {'body': full_body}, minimal_qcontext=True),
'is_notification': True,
'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
+2 -2
View File
@@ -1875,7 +1875,7 @@ class MrpProduction(models.Model):
'impacted_pickings': False,
'cancel': cancel
}
return self.env.ref('mrp.exception_on_mo')._render(values=values)
return self.env['ir.qweb']._render('mrp.exception_on_mo', values)
documents = self.env['stock.picking']._log_activity_get_documents(moves_modification, 'move_dest_ids', 'DOWN', _keys_in_groupby)
documents = self.env['stock.picking']._less_quantities_than_expected_add_documents(moves_modification, documents)
@@ -1901,7 +1901,7 @@ class MrpProduction(models.Model):
'impacted_object': impacted_object,
'cancel': cancel
}
return self.env.ref('mrp.exception_on_mo')._render(values=values)
return self.env['ir.qweb']._render('mrp.exception_on_mo', values)
self.env['stock.picking']._log_activity(_render_note_exception_quantity_mo, documents)
@@ -45,13 +45,13 @@ class ReportBomStructure(models.AbstractModel):
res['lines']['report_type'] = 'html'
res['lines']['report_structure'] = 'all'
res['lines']['has_attachments'] = res['lines']['attachments'] or any(component['attachments'] for component in res['lines']['components'])
res['lines'] = self.env.ref('mrp.report_mrp_bom')._render({'data': res['lines']})
res['lines'] = self.env['ir.qweb']._render('mrp.report_mrp_bom', {'data': res['lines']})
return res
@api.model
def get_bom(self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False):
lines = self._get_bom(bom_id=bom_id, product_id=product_id, line_qty=line_qty, line_id=line_id, level=level)
return self.env.ref('mrp.report_mrp_bom_line')._render({'data': lines})
return self.env['ir.qweb']._render('mrp.report_mrp_bom_line', {'data': lines})
@api.model
def get_operations(self, product_id=False, bom_id=False, qty=0, level=0):
@@ -64,7 +64,7 @@ class ReportBomStructure(models.AbstractModel):
'operations': lines,
'extra_column_count': self._get_extra_column_count()
}
return self.env.ref('mrp.report_mrp_operation_line')._render({'data': values})
return self.env['ir.qweb']._render('mrp.report_mrp_operation_line', {'data': values})
@api.model
def get_byproducts(self, bom_id=False, qty=0, level=0, total=0):
@@ -76,7 +76,7 @@ class ReportBomStructure(models.AbstractModel):
'byproducts': lines,
'extra_column_count': self._get_extra_column_count(),
}
return self.env.ref('mrp.report_mrp_byproduct_line')._render({'data': values})
return self.env['ir.qweb']._render('mrp.report_mrp_byproduct_line', {'data': values})
@api.model
def _get_report_data(self, bom_id, searchQty=0, searchVariant=False):
+1 -1
View File
@@ -493,7 +493,7 @@ class PaymentTransaction(models.Model):
"%(ref)s:\n%(values)s",
{'ref': self.reference, 'values': pprint.pformat(rendering_values)},
)
redirect_form_html = redirect_form_view._render(rendering_values, engine='ir.qweb')
redirect_form_html = self.env['ir.qweb']._render(redirect_form_view.id, rendering_values)
processing_values.update(redirect_form_html=redirect_form_html)
return processing_values
@@ -51,11 +51,12 @@ class PaymentAcquirer(models.Model):
return 'https://www.sandbox.paypal.com/cgi-bin/webscr'
def _paypal_send_configuration_reminder(self):
template = self.env.ref(
'payment_paypal.mail_template_paypal_invite_user_to_configure', raise_if_not_found=False
render_template = self.env['ir.qweb']._render(
'payment_paypal.mail_template_paypal_invite_user_to_configure',
{'acquirer': self},
raise_if_not_found=False,
)
if template:
render_template = template._render({'acquirer': self}, engine='ir.qweb')
if render_template:
mail_body = self.env['mail.render.mixin']._replace_local_links(render_template)
mail_values = {
'body_html': mail_body,
+1
View File
@@ -3,6 +3,7 @@
from . import ir_http
from . import ir_ui_view
from . import ir_qweb
from . import mail_thread
from . import mail_message
from . import portal_mixin
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools import is_html_empty
class IrQWeb(models.AbstractModel):
_inherit = "ir.qweb"
def _prepare_frontend_environment(self, values):
""" Returns ir.qweb with context and update values with portal specific
value (required to render portal layout template)
"""
irQweb = super()._prepare_frontend_environment(values)
values.update(
is_html_empty=is_html_empty,
languages=[lang for
lang in irQweb.env['res.lang'].get_available()
if lang[0] in irQweb.env['ir.http']._get_frontend_langs()]
)
for key in irQweb.env.context:
if key not in values:
values[key] = irQweb.env.context[key]
return irQweb
+1 -21
View File
@@ -1,30 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.tools import is_html_empty
from odoo import models, fields
class View(models.Model):
_inherit = "ir.ui.view"
customize_show = fields.Boolean("Show As Optional Inherit", default=False)
@api.model
def _prepare_qcontext(self):
""" Returns the qcontext : rendering context with portal specific value (required
to render portal layout template)
"""
qcontext = super(View, self)._prepare_qcontext()
if request and getattr(request, 'is_frontend', False):
Lang = request.env['res.lang']
portal_lang_code = request.env['ir.http']._get_frontend_langs()
qcontext.update(dict(
self._context.copy(),
languages=[lang for lang in Lang.get_available() if lang[0] in portal_lang_code],
url_for=url_for,
is_html_empty=is_html_empty,
))
return qcontext
@@ -46,7 +46,7 @@ class MercuryTransaction(models.Model):
data['memo'] = "Odoo " + service.common.exp_version()['server_version']
def _do_request(self, template, data):
xml_transaction = self.env.ref(template)._render(data)
xml_transaction = self.env['ir.qweb']._render(template, data)
if not data['merchant_id'] or not data['merchant_pwd']:
return "not setup"
@@ -14,7 +14,7 @@ class ProductPricelistReport(models.AbstractModel):
@api.model
def get_html(self, data):
render_values = self._get_report_data(data, 'html')
return self.env.ref('product.report_pricelist_page')._render(render_values)
return self.env['ir.qweb']._render('product.report_pricelist_page', render_values)
def _get_report_data(self, data, report_type='html'):
quantities = data['quantities'] or [1]
+1 -2
View File
@@ -1871,7 +1871,6 @@ class Task(models.Model):
template_id = self.env['ir.model.data']._xmlid_to_res_id('project.project_message_user_assigned', raise_if_not_found=False)
if not template_id:
return
view = self.env['ir.ui.view'].browse(template_id)
task_model_description = self.env['ir.model']._get(self._name).display_name
for task, users in users_per_task.items():
if not users:
@@ -1883,7 +1882,7 @@ class Task(models.Model):
}
for user in users:
values.update(assignee_name=user.sudo().name)
assignation_msg = view._render(values, engine='ir.qweb', minimal_qcontext=True)
assignation_msg = self.env['ir.qweb']._render('project.project_message_user_assigned', values, minimal_qcontext=True)
assignation_msg = self.env['mail.render.mixin']._replace_local_links(assignation_msg)
task.message_notify(
subject=_('You have been assigned to %s', task.display_name),
+1 -2
View File
@@ -94,8 +94,7 @@ class ProjectUpdate(models.Model):
# ---------------------------------
@api.model
def _build_description(self, project):
template = self.env.ref('project.project_update_default_description')
return template._render(self._get_template_values(project), engine='ir.qweb')
return self.env['ir.qweb']._render('project.project_update_default_description', self._get_template_values(project))
@api.model
def _get_template_values(self, project):
+1 -1
View File
@@ -167,7 +167,7 @@ class PurchaseOrder(models.Model):
'order_exceptions': order_exceptions.values(),
'impacted_pickings': impacted_pickings,
}
return self.env.ref('purchase_stock.exception_on_po')._render(values=values)
return self.env['ir.qweb']._render('purchase_stock.exception_on_po', values)
documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_groupby)
filtered_documents = {}
+1 -1
View File
@@ -19,7 +19,7 @@ class OnboardingController(http.Controller):
return {}
return {
'html': request.env.ref('sale.sale_quotation_onboarding_panel')._render({
'html': request.env['ir.qweb']._render('sale.sale_quotation_onboarding_panel', {
'company': company,
'state': company.get_and_update_sale_quotation_onboarding_state()
})
+1 -1
View File
@@ -232,7 +232,7 @@ class SaleOrder(models.Model):
'impacted_pickings': impacted_pickings,
'cancel': cancel
}
return self.env.ref('sale_stock.exception_on_so')._render(values=values)
return self.env['ir.qweb']._render('sale_stock.exception_on_so', values)
self.env['stock.picking']._log_activity(_render_note_exception_quantity_so, documents)
+1 -1
View File
@@ -145,7 +145,7 @@ class StockPicking(models.Model):
'origin_picking': origin_picking,
'moves_information': moves_information.values(),
}
return self.env.ref('sale_stock.exception_on_picking')._render(values=values)
return self.env['ir.qweb']._render('sale_stock.exception_on_picking', values)
documents = self._log_activity_get_documents(moves, 'sale_line_id', 'DOWN', _keys_in_groupby)
self._log_activity(_render_note_exception_quantity, documents)
-1
View File
@@ -2,7 +2,6 @@
from . import iap_account
from . import ir_actions_report
from . import ir_qweb_fields
from . import mail_message
from . import mail_notification
from . import res_company
-19
View File
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class Contact(models.AbstractModel):
_inherit = 'ir.qweb.field.contact'
@api.model
def value_to_html(self, value, options):
if self.env.context.get('snailmail_layout'):
value = value.with_context(snailmail_layout=self.env.context['snailmail_layout'])
return super(Contact, self).value_to_html(value, options)
@api.model
def record_to_html(self, record, field_name, options):
if self.env.context.get('snailmail_layout'):
record = record.with_context(snailmail_layout=self.env.context['snailmail_layout'])
return super(Contact, self).record_to_html(record, field_name, options)
+1 -1
View File
@@ -1300,7 +1300,7 @@ class Picking(models.Model):
'moves_information': rendering_context.values(),
'impacted_pickings': impacted_pickings,
}
return self.env.ref('stock.exception_on_picking')._render(values=values)
return self.env['ir.qweb']._render('stock.exception_on_picking', values)
documents = self._log_activity_get_documents(moves, 'move_dest_ids', 'DOWN', _keys_in_groupby)
documents = self._less_quantities_than_expected_add_documents(moves, documents)
+1 -1
View File
@@ -236,7 +236,7 @@ class MrpStockReport(models.TransientModel):
rcontext = {}
context = dict(self.env.context)
rcontext['lines'] = self.with_context(context).get_lines()
result['html'] = self.env.ref('stock.report_stock_inventory')._render(rcontext)
result['html'] = self.env['ir.qweb']._render('stock.report_stock_inventory', rcontext)
return result
@api.model
+6 -6
View File
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields, http, SUPERUSER_ID, _
from odoo.addons.base.models.ir_ui_view import keep_query
from odoo.addons.http_routing.models.ir_qweb import keep_query
from odoo.exceptions import UserError
from odoo.http import request, content_disposition
from odoo.osv import expression
@@ -346,22 +346,22 @@ class Survey(http.Controller):
survey_data = self._prepare_survey_data(survey_sudo, answer_sudo, **post)
if answer_sudo.state == 'done':
survey_content = request.env.ref('survey.survey_fill_form_done')._render(survey_data)
survey_content = request.env['ir.qweb']._render('survey.survey_fill_form_done', survey_data)
else:
survey_content = request.env.ref('survey.survey_fill_form_in_progress')._render(survey_data)
survey_content = request.env['ir.qweb']._render('survey.survey_fill_form_in_progress', survey_data)
survey_progress = False
if answer_sudo.state == 'in_progress' and not survey_data.get('question', request.env['survey.question']).is_page:
if survey_sudo.questions_layout == 'page_per_section':
page_ids = survey_sudo.page_ids.ids
survey_progress = request.env.ref('survey.survey_progression')._render({
survey_progress = request.env['ir.qweb']._render('survey.survey_progression', {
'survey': survey_sudo,
'page_ids': page_ids,
'page_number': page_ids.index(survey_data['page'].id) + (1 if survey_sudo.progression_mode == 'number' else 0)
})
elif survey_sudo.questions_layout == 'page_per_question':
page_ids = survey_sudo.question_ids.ids
survey_progress = request.env.ref('survey.survey_progression')._render({
survey_progress = request.env['ir.qweb']._render('survey.survey_progression', {
'survey': survey_sudo,
'page_ids': page_ids,
'page_number': page_ids.index(survey_data['question'].id)
@@ -376,7 +376,7 @@ class Survey(http.Controller):
return {
'survey_content': survey_content,
'survey_progress': survey_progress,
'survey_navigation': request.env.ref('survey.survey_navigation')._render(survey_data),
'survey_navigation': request.env['ir.qweb']._render('survey.survey_navigation', survey_data),
'background_image_url': background_image_url
}
@@ -120,7 +120,7 @@ class UserInputSession(http.Controller):
return {
'background_image_url': survey.session_question_id.background_image_url,
'question_html': request.env.ref('survey.user_input_session_manage_content')._render(template_values)
'question_html': request.env['ir.qweb']._render('survey.user_input_session_manage_content', template_values)
}
else:
return {}
@@ -159,7 +159,7 @@ class UserInputSession(http.Controller):
# no open session
return ''
return request.env.ref('survey.user_input_session_leaderboard')._render({
return request.env['ir.qweb']._render('survey.user_input_session_leaderboard', {
'animate': True,
'leaderboard': survey._prepare_leaderboard_values()
})
+9 -11
View File
@@ -215,18 +215,16 @@ class SurveyInvite(models.TransientModel):
# optional support of default_email_layout_xmlid in context
email_layout_xmlid = self.env.context.get('default_email_layout_xmlid', self.env.context.get('notif_layout'))
if email_layout_xmlid:
try:
template = self.env.ref(email_layout_xmlid, raise_if_not_found=True)
except ValueError:
_logger.warning('QWeb template %s not found when sending survey mails. Sending without layout', email_layout_xmlid)
else:
template_ctx = {
'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.survey_id.title)),
'model_description': self.env['ir.model']._get('survey.survey').display_name,
'company': self.env.company,
}
body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
template_ctx = {
'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.survey_id.title)),
'model_description': self.env['ir.model']._get('survey.survey').display_name,
'company': self.env.company,
}
body = self.env['ir.qweb']._render(email_layout_xmlid, template_ctx, minimal_qcontext=True, raise_if_not_found=False)
if body:
mail_values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
else:
_logger.warning('QWeb template %s not found or is empty when sending survey mails. Sending without layout', email_layout_xmlid)
return self.env['mail.mail'].sudo().create(mail_values)
@@ -52,7 +52,7 @@ class TestEventPerformance(EventPerformanceCase):
batch_size = 20
# simple without type involved + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=5495): # tef only: 5069 (5065) - com runbot: 5069 - ent runbot 5489-5494
with freeze_time(self.reference_now), self.assertQueryCount(event_user=5484): # tef only: 5069 (5065) - com runbot: 5059 - ent runbot 5484
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = [
dict(self.event_base_vals,
@@ -89,7 +89,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=5608): # tef only: 5178 - com runbot: 5182 - ent runbot 5602-5607
with freeze_time(self.reference_now), self.assertQueryCount(event_user=5597): # tef only: 5178 - com runbot: 5172 - ent runbot 5597
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = [
dict(self.event_base_vals,
@@ -125,7 +125,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# no type, website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=750): # tef only: 637 - com runbot: 638
with freeze_time(self.reference_now), self.assertQueryCount(event_user=691): # tef only: 637 - com runbot: 587 - ent runbot: 691
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.event']) as event_form:
event_form.name = 'Test Event'
@@ -144,7 +144,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# type and website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=783): # tef only: 673 - com runbot: 676
with freeze_time(self.reference_now), self.assertQueryCount(event_user=724): # tef only: 673 - com runbot: 624 - ent runbot: 724
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.event']) as event_form:
event_form.name = 'Test Event'
@@ -172,7 +172,7 @@ class TestEventPerformance(EventPerformanceCase):
def test_event_create_single_notype_website(self):
""" Test a single event creation """
# simple without type involved + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=384): # tef only: 358 (353) - com runbot: 357 - ent runbot 383
with freeze_time(self.reference_now), self.assertQueryCount(event_user=373): # tef only: 358 (353) - com runbot: 284 - ent runbot 373
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
@@ -203,7 +203,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type + website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=420): # tef only: 395 (389) - com runbot: 393 - ent runbot 419
with freeze_time(self.reference_now), self.assertQueryCount(event_user=409): # tef only: 395 (389) - com runbot: 383 - ent runbot 409
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
@@ -225,7 +225,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
"""
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=721): # tef only: 674 - com runbot 717 - ent runbot 720
with freeze_time(self.reference_now), self.assertQueryCount(event_user=716): # tef only: 674 - com runbot 713 - ent runbot 716
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = [
dict(reg_data,
@@ -271,7 +271,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
form like) """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=731): # tef only: 685 - com runbot 728
with freeze_time(self.reference_now), self.assertQueryCount(event_user=727): # tef only: 685 - com runbot 724 - ent runbot: 727
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = [
dict(reg_data,
@@ -292,7 +292,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
""" Test a single registration creation using Form """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=230): # tef only: 210 - com runbot 216
with freeze_time(self.reference_now), self.assertQueryCount(event_user=227): # tef only: 210 - com runbot: 213 - ent runbot: 227
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.registration']) as reg_form:
reg_form.event_id = event
@@ -308,7 +308,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
""" Test a single registration creation using Form """
event = self.env['event.event'].browse(self.test_event.ids)
with freeze_time(self.reference_now), self.assertQueryCount(event_user=233): # tef only: 213 - com runbot 218
with freeze_time(self.reference_now), self.assertQueryCount(event_user=229): # tef only: 213 - com runbot: 214 - ent runbot: 229
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.registration']) as reg_form:
reg_form.event_id = event
@@ -335,7 +335,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# simple customer data
with freeze_time(self.reference_now), self.assertQueryCount(event_user=142): # tef only: 135 - com runbot 140
with freeze_time(self.reference_now), self.assertQueryCount(event_user=139): # tef only: 135 - com runbot: 137 - ent runbot: 139
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = dict(
self.customer_data[0],
@@ -349,7 +349,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# partner-based customer
with freeze_time(self.reference_now), self.assertQueryCount(event_user=149): # tef only: 143 - com runbot 148
with freeze_time(self.reference_now), self.assertQueryCount(event_user=145): # tef only: 143 - com runbot: 144 - ent runbot: 145
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = {
'event_id': event.id,
@@ -364,7 +364,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# partner-based customer
with freeze_time(self.reference_now), self.assertQueryCount(event_user=60): # tef only: 56 - com runbot 58
with freeze_time(self.reference_now), self.assertQueryCount(event_user=147): # tef only: 56 - com runbot: 58 - ent runbot: 147
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = {
'event_id': event.id,
@@ -379,7 +379,7 @@ class TestRegistrationPerformance(EventPerformanceCase):
event = self.env['event.event'].browse(self.test_event.ids)
# website customer data
with freeze_time(self.reference_now), self.assertQueryCount(event_user=151): # tef only: 142 - com runbot 147
with freeze_time(self.reference_now), self.assertQueryCount(event_user=147): # tef only: 142 - com runbot: 143 - ent runbot: 147
self.env.cr._now = self.reference_now # force create_date to check schedulers
registration_values = dict(
self.website_customer_data[0],
+2 -2
View File
@@ -243,7 +243,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
# voip module read activity_type during create leading to one less query in enterprise on action_feedback
_category = activity.activity_type_id.category
with self.assertQueryCount(__system__=17, employee=19):
with self.assertQueryCount(__system__=16, employee=18):
activity.action_feedback(feedback='Zizisse Done !')
@users('__system__', 'employee')
@@ -260,7 +260,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
record.write({'name': 'Dupe write'})
with self.assertQueryCount(__system__=18, employee=21):
with self.assertQueryCount(__system__=17, employee=20):
record.action_close('Dupe feedback')
self.assertEqual(record.activity_ids, self.env['mail.activity'])
+1 -1
View File
@@ -1058,7 +1058,7 @@ class Database(http.Controller):
with file_open("web/static/src/public/database_manager.create_form.qweb.html", "r") as fd:
templates['create_form'] = fd.read()
def load(template_name, options):
def load(template_name):
fromstring = html.document_fromstring if template_name == 'database_manager' else html.fragment_fromstring
return (fromstring(templates[template_name]), template_name)
+2 -6
View File
@@ -252,13 +252,9 @@ class BaseDocumentLayout(models.TransientModel):
'_get_css_for_preview' processing later.
:return:
"""
template_style = self.env.ref('web.styles_company_report', raise_if_not_found=False)
if not template_style:
return b''
company_styles = template_style._render({
company_styles = self.env['ir.qweb']._render('web.styles_company_report', {
'company_ids': self,
})
}, raise_if_not_found=False)
return company_styles
+1 -1
View File
@@ -94,7 +94,7 @@ class Image(models.AbstractModel):
atts["data-zoom-image"] = src_zoom
atts["data-no-post-process"] = options.get('data-no-post-process')
atts = self.env['ir.qweb']._post_processing_att('img', atts, options.get('template_options'))
atts = self.env['ir.qweb']._post_processing_att('img', atts)
img = ['<img']
for name, value in atts.items():
+12 -23
View File
@@ -226,23 +226,15 @@ class Base(models.AbstractModel):
def qweb_render_view(self, view_id, domain):
assert view_id
return self.env['ir.qweb']._render(
view_id, {
**self.env['ir.ui.view']._prepare_qcontext(),
**self._qweb_prepare_qcontext(view_id, domain),
})
def _qweb_prepare_qcontext(self, view_id, domain):
"""
Base qcontext for rendering qweb views bound to this model
"""
return {
'model': self,
'domain': domain,
# not necessarily necessary as env is already part of the
# non-minimal qcontext
'context': self.env.context,
'records': lazy(self.search, domain),
}
view_id,
{
'model': self,
'domain': domain,
# not necessarily necessary as env is already part of the
# non-minimal qcontext
'context': self.env.context,
'records': lazy(self.search, domain),
})
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
@@ -800,15 +792,12 @@ class ResCompany(models.Model):
return res
def _get_asset_style_b64(self):
template_style = self.env.ref('web.styles_company_report', raise_if_not_found=False)
if not template_style:
return b''
# One bundle for everyone, so this method
# necessarily updates the style for every company at once
company_ids = self.sudo().search([])
company_styles = template_style._render({
'company_ids': company_ids,
})
company_styles = self.env['ir.qweb']._render('web.styles_company_report', {
'company_ids': company_ids,
}, raise_if_not_found=False)
return base64.b64encode(company_styles.encode())
def _update_asset_style(self):
@@ -226,7 +226,6 @@ class Contact(models.AbstractModel):
def attributes(self, record, field_name, options, values):
attrs = super(Contact, self).attributes(record, field_name, options, values)
if options.get('inherit_branding'):
options.pop('template_options') # remove options not specific to this widget
attrs['data-oe-contact-options'] = json.dumps(options)
return attrs
-11
View File
@@ -6,7 +6,6 @@ import logging
import uuid
from lxml import etree, html
from odoo.exceptions import AccessError
from odoo import api, models
from odoo.osv import expression
@@ -18,16 +17,6 @@ EDITING_ATTRIBUTES = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-x
class IrUiView(models.Model):
_inherit = 'ir.ui.view'
def _render(self, values=None, engine='ir.qweb', minimal_qcontext=False, options=None):
if values and values.get('editable'):
try:
self.check_access_rights('write')
self.check_access_rule('write')
except AccessError:
values['editable'] = False
return super(IrUiView, self)._render(values=values, engine=engine, minimal_qcontext=minimal_qcontext, options=options)
#------------------------------------------------------
# Save from html
#------------------------------------------------------
-2
View File
@@ -436,8 +436,6 @@ class Website(Home):
opt = {}
if field_type == 'monetary':
opt['display_currency'] = options['display_currency']
elif field_type == 'html':
opt['template_options'] = {}
value = request.env[('ir.qweb.field.%s' % field_type)].value_to_html(value, opt)
mapped[mapped_name] = escape(value)
result.append(mapped)
+1 -1
View File
@@ -382,7 +382,7 @@ class Http(models.AbstractModel):
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)
# Needed to show reset template on translated pages (`_prepare_environment` will set it for main lang)
values['editable'] = request.uid and request.website.is_publisher()
return values
+69 -7
View File
@@ -9,7 +9,6 @@ from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.osv import expression
from odoo.addons.website.models import ir_http
from odoo.tools import html_escape as escape
re_background_image = re.compile(r"(background-image\s*:\s*url\(\s*['\"]?\s*)([^)'\"]+)")
@@ -47,23 +46,86 @@ class IrQWeb(models.AbstractModel):
'img': 'src',
}
# assume cache will be invalidated by third party on write to ir.ui.view
def _get_template_cache_keys(self):
""" Return the list of context keys to use for caching ``_compile``. """
return super()._get_template_cache_keys() + ['website_id']
def _prepare_frontend_environment(self, values):
""" Update the values and context with website specific value
(required to render website layout template)
"""
irQweb = super()._prepare_frontend_environment(values)
Website = irQweb.env['website']
editable = request.website.is_publisher()
translatable = editable and irQweb.env.context.get('lang') != irQweb.env['ir.http']._get_default_lang().code
editable = not translatable and editable
current_website = Website.get_current_website()
has_group_publisher = irQweb.env.user.has_group('website.group_website_publisher')
if has_group_publisher and irQweb.env.user.has_group('website.group_multi_website'):
values['multi_website_websites_current'] = current_website.name
values['multi_website_websites'] = [
{'website_id': website.id, 'name': website.name, 'domain': website.domain}
for website in Website.search([('id', '!=', current_website.id)])
]
cur_company = irQweb.env.company
values['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name}
values['multi_website_companies'] = [
{'company_id': comp.id, 'name': comp.name}
for comp in irQweb.env.user.company_ids if comp != cur_company
]
# update values
values.update(dict(
website=current_website,
is_view_active=current_website.is_view_active,
res_company=request.env['res.company'].browse(current_website._get_cached('company_id')).sudo(),
translatable=translatable,
editable=editable,
))
if editable:
# form editable object, add the backend configuration link
if 'main_object' in values and has_group_publisher:
func = getattr(values['main_object'], 'get_backend_menu_id', False)
values['backend_menu_id'] = func and func() or irQweb.env['ir.model.data']._xmlid_to_res_id('website.menu_website_configuration')
# update options
irQweb = irQweb.with_context(website_id=current_website.id)
if 'inherit_branding' not in irQweb.env.context and not self.env.context.get('rendering_bundle'):
if editable:
# in edit mode add brancding on ir.ui.view tag nodes
irQweb = irQweb.with_context(inherit_branding=True)
elif has_group_publisher and not translatable:
# will add the branding on fields (into values)
irQweb = irQweb.with_context(inherit_branding_auto=True)
return irQweb
def _get_asset_bundle(self, xmlid, files, env=None, css=True, js=True):
return AssetsBundleMultiWebsite(xmlid, files, env=env)
def _post_processing_att(self, tagName, atts, options):
def _post_processing_att(self, tagName, atts):
if atts.get('data-no-post-process'):
return atts
atts = super(IrQWeb, self)._post_processing_att(tagName, atts, options)
atts = super()._post_processing_att(tagName, atts)
website = ir_http.get_request_website()
if not website and options.get('website_id'):
website = self.env['website'].browse(options['website_id'])
if not website and self.env.context.get('website_id'):
website = self.env['website'].browse(self.env.context['website_id'])
if website and tagName == 'img' and 'loading' not in atts:
atts['loading'] = 'lazy' # default is auto
if options.get('inherit_branding') or options.get('rendering_bundle') or \
options.get('edit_translations') or options.get('debug') or (request and request.session.debug):
if self.env.context.get('inherit_branding') or self.env.context.get('rendering_bundle') or \
self.env.context.get('edit_translations') or self.env.context.get('debug') or (request and request.session.debug):
return atts
if not website:
+8 -66
View File
@@ -407,73 +407,15 @@ class View(models.Model):
return False
return True
def _render(self, values=None, engine='ir.qweb', minimal_qcontext=False, options=None):
def _render_template(self, template, values=None):
""" Render the template. If website is enabled on request, then extend rendering context with website values. """
self._handle_visibility(do_raise=True)
new_context = dict(self._context)
if request and getattr(request, 'is_frontend', False):
editable = request.website.is_publisher()
translatable = editable and self._context.get('lang') != request.website.default_lang_id.code
editable = not translatable and editable
# in edit mode ir.ui.view will tag nodes
if not translatable and not self.env.context.get('rendering_bundle'):
if editable:
new_context = dict(self._context, inherit_branding=True)
elif request.env.user.has_group('website.group_website_publisher'):
new_context = dict(self._context, inherit_branding_auto=True)
if values and 'main_object' in values:
if request.env.user.has_group('website.group_website_publisher'):
func = getattr(values['main_object'], 'get_backend_menu_id', False)
values['backend_menu_id'] = func and func() or self.env['ir.model.data']._xmlid_to_res_id('website.menu_website_configuration')
if self._context != new_context:
self = self.with_context(new_context)
return super(View, self)._render(values, engine=engine, minimal_qcontext=minimal_qcontext, options=options)
@api.model
def _prepare_qcontext(self):
""" Returns the qcontext : rendering context with website specific value (required
to render website layout template)
"""
qcontext = super(View, self)._prepare_qcontext()
if request and getattr(request, 'is_frontend', False):
Website = self.env['website']
editable = request.website.is_publisher()
translatable = editable and self._context.get('lang') != request.env['ir.http']._get_default_lang().code
editable = not translatable and editable
cur = Website.get_current_website()
if self.env.user.has_group('website.group_website_publisher') and self.env.user.has_group('website.group_multi_website'):
qcontext['multi_website_websites_current'] = cur.name
qcontext['multi_website_websites'] = [
{'website_id': website.id, 'name': website.name, 'domain': website.domain}
for website in Website.search([]) if website != cur
]
cur_company = self.env.company
qcontext['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name}
qcontext['multi_website_companies'] = [
{'company_id': comp.id, 'name': comp.name}
for comp in self.env.user.company_ids if comp != cur_company
]
qcontext.update(dict(
main_object=self,
website=request.website,
is_view_active=request.website.is_view_active,
res_company=request.env['res.company'].browse(request.website._get_cached('company_id')).sudo(),
translatable=translatable,
editable=editable,
))
# fetch non-prefetchable fields of the mixin SeoMetadata, as they
# will be read later in non-sudo mode
self.sudo().read(['website_meta_title', 'website_meta_description', 'website_meta_keywords'])
return qcontext
view = self.sudo().browse(self.get_view_id(template))
view._handle_visibility(do_raise=True)
if values is None:
values = {}
if 'main_object' not in values:
values['main_object'] = view
return super()._render_template(template, values=values)
@api.model
def get_default_lang_code(self):
+8 -8
View File
@@ -430,9 +430,10 @@ class Website(models.Model):
nb_snippets = len(snippet_list)
for i, snippet in enumerate(snippet_list, start=1):
try:
view_id = self.env['website'].with_context(website_id=website.id, lang=website.default_lang_id.code).viewref('website.' + snippet)
if view_id:
el = html.fromstring(view_id._render(values=cta_data))
IrQweb = self.env['ir.qweb'].with_context(website_id=website.id, lang=website.default_lang_id.code)
render = IrQweb._render('website.' + snippet, cta_data)
if render:
el = html.fromstring(render)
# Add the data-snippet attribute to identify the snippet
# for compatibility code
@@ -604,12 +605,12 @@ class Website(models.Model):
return
# keep strange indentation in python file, to get it correctly in database
new_homepage_view = '''<t name="Homepage" t-name="website.homepage%s">
new_homepage_view = '''<t name="Homepage" t-name="website.homepage">
<t t-call="website.layout">
<t t-set="pageName" t-value="'homepage'"/>
<div id="wrap" class="oe_structure oe_empty"/>
</t>
</t>''' % (self.id)
</t>'''
standard_homepage.with_context(website_id=self.id).arch_db = new_homepage_view
homepage_page = Page.search([
@@ -1415,9 +1416,8 @@ class Website(models.Model):
# Check snippet template definition to avoid disabling its related assets.
# This special case is needed because snippet template definitions do not
# have a `data-snippet` attribute (which is added during drag&drop).
snippet_template = self.env.ref(f'{snippet_module}.{snippet_id}', raise_if_not_found=False)
if snippet_template:
snippet_template_html = snippet_template._render()
snippet_template_html = self.env['ir.qweb']._render(f'{snippet_module}.{snippet_id}', raise_if_not_found=False)
if snippet_template_html:
match = re.search('<([^>]*class="[^>]*)>', snippet_template_html)
snippet_occurences.append(match.group())
@@ -71,8 +71,7 @@ class WebsiteSnippetFilter(models.Model):
is_sample = with_sample and not records
if is_sample:
records = self._prepare_sample(limit)
View = self.env['ir.ui.view'].sudo().with_context(inherit_branding=False)
content = View._render_template(template_key, dict(
content = self.env['ir.qweb'].with_context(inherit_branding=False)._render(template_key, dict(
records=records,
is_sample=is_sample,
))
+1 -1
View File
@@ -116,7 +116,7 @@ class TestWebsitePerformance(UtilPerf):
# website.page with no call to layout templates
self.page.arch = '<div>I am a blank page</div>'
self.assertEqual(self._get_url_hot_query(self.page.url), 5)
self.assertEqual(self._get_url_hot_query(self.page.url, cache=False), 6)
self.assertEqual(self._get_url_hot_query(self.page.url, cache=False), 5)
def test_40_perf_sql_queries_page_multi_level_menu(self):
# menu structure should not impact SQL requests
+1 -1
View File
@@ -108,7 +108,7 @@ class TestQwebProcessAtt(TransactionCase):
def _test_att(self, url, expect, tag='a', attribute='href'):
self.assertEqual(
self.env['ir.qweb']._post_processing_att(tag, {attribute: url}, {}),
self.env['ir.qweb']._post_processing_att(tag, {attribute: url}),
expect
)
+2 -2
View File
@@ -211,7 +211,7 @@ class TestViewSaving(TestViewSavingCommon):
)
self.assertIn(
replacement,
view._render(),
self.env['ir.qweb']._render(view.id),
'inline script should not be escaped when rendering'
)
# common text nodes should be be escaped client side
@@ -220,7 +220,7 @@ class TestViewSaving(TestViewSavingCommon):
self.assertIn(replacement, view.arch, 'common text node should not be escaped server side')
self.assertIn(
replacement,
str(view._render()).replace(u'&', u'&amp;'),
str(self.env['ir.qweb']._render(view.id)).replace(u'&', u'&amp;'),
'text node characters wrongly unescaped when rendering'
)
@@ -28,8 +28,8 @@ class TestBlogPerformance(UtilPerf):
blog_tags = blog_tags[:-1]
self.assertEqual(self._get_url_hot_query('/blog'), 26)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 25)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 31)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 30)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 30)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 29)
def test_30_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post']
@@ -173,8 +173,8 @@
<!-- Short Date -->
<div class="o_wevent_event_date position-absolute bg-white shadow-sm text-dark">
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'format': 'LLL'}" class="o_wevent_event_month"/>
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'format': 'dd'}" class="o_wevent_event_day oe_hide_on_date_edit"/>
<span t-field="event.date_begin" t-options="{'tz_name': event.date_tz, 'format': 'LLL'}" class="o_wevent_event_month"/>
<span t-field="event.date_begin" t-options="{'tz_name': event.date_tz, 'format': 'dd'}" class="o_wevent_event_day oe_hide_on_date_edit"/>
</div>
<!-- Participating -->
<small t-if="event.is_participating" class="o_wevent_participating bg-success">
@@ -198,10 +198,10 @@
</h5>
<!-- Start Date & Time -->
<time itemprop="startDate" t-att-datetime="event.date_begin">
<span t-field="event.with_context(tz=event.date_tz).date_begin"
t-options="{'date_only': 'true', 'format': 'long', 'tz_name': event.date_tz}"/> -
<span t-field="event.with_context(tz=event.date_tz).date_begin" class="oe_hide_on_date_edit"
t-options="{'time_only': 'true', 'format': 'short', 'tz_name': event.date_tz}"/>
<span t-field="event.date_begin"
t-options="{'tz_name': event.date_tz, 'date_only': 'true', 'format': 'long', 'tz_name': event.date_tz}"/> -
<span t-field="event.date_begin" class="oe_hide_on_date_edit"
t-options="{'tz_name': event.date_tz, 'time_only': 'true', 'format': 'short', 'tz_name': event.date_tz}"/>
(<span t-field="event.date_tz"/>)
</time>
<!-- Location -->
@@ -42,25 +42,25 @@
<div class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Date &amp; Time</h6>
<div class="d-flex">
<h5 t-field="event.with_context(tz=event.date_tz).date_begin" class="my-1 mr-1 oe_hide_on_date_edit" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1 oe_hide_on_date_edit" t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'date_only': 'true', 'format': 'long'}" itemprop="startDate" t-att-datetime="event.date_begin"/>
<h5 t-field="event.date_begin" class="my-1 mr-1 oe_hide_on_date_edit" t-options="{'tz_name': event.date_tz, 'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1 oe_hide_on_date_edit" t-field="event.date_begin" t-options="{'tz_name': event.date_tz, 'date_only': 'true', 'format': 'long'}" itemprop="startDate" t-att-datetime="event.date_begin"/>
</div>
<t t-if="not event.is_one_day">Start -</t>
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'time_only': 'true', 'format': 'short'}"/>
<span t-field="event.date_begin" t-options="{'tz_name': event.date_tz, 'time_only': 'true', 'format': 'short'}"/>
<t t-if="event.is_one_day">
<i class="fa fa-long-arrow-right mx-1"/>
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'time_only': 'true', 'format': 'short'}"/>
<span t-field="event.date_end" t-options="{'tz_name': event.date_tz, 'time_only': 'true', 'format': 'short'}"/>
(<span t-out="event.date_tz"/>)
</t>
<t t-else="">
(<span t-out="event.date_tz"/>)
<i class="fa fa-long-arrow-down d-block text-muted mx-3 my-2" style="font-size: 1.5rem"/>
<div class="d-flex">
<h5 t-field="event.with_context(tz=event.date_tz).date_end" class="my-1 mr-1 oe_hide_on_date_edit" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1 oe_hide_on_date_edit" t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'date_only': 'true', 'format': 'long'}"/>
<h5 t-field="event.date_end" class="my-1 mr-1 oe_hide_on_date_edit" t-options="{'tz_name': event.date_tz, 'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1 oe_hide_on_date_edit" t-field="event.date_end" t-options="{'tz_name': event.date_tz, 'date_only': 'true', 'format': 'long'}"/>
</div>
<t t-if="not event.is_one_day">End -</t>
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'time_only': 'true', 'format': 'short'}"/>
<span t-field="event.date_end" t-options="{'tz_name': event.date_tz, 'time_only': 'true', 'format': 'short'}"/>
(<span t-out="event.date_tz"/>)
</t>
@@ -50,8 +50,8 @@
</span>
<span class="my-0" t-else="">
starts on
<span t-field="sponsor.event_id.with_context(tz=sponsor.event_id.date_tz).date_begin"
t-options="{'format': 'medium'}"/> (<t t-out="sponsor.event_id.date_tz"/>).
<span t-field="sponsor.event_id.date_begin"
t-options="{'format': 'medium', 'tz_name': sponsor.event_id.date_tz}"/> (<t t-out="sponsor.event_id.date_tz"/>).
</span>
<br/>
<span t-if="is_event_user">Attendees will be able to join to meet <b t-out="sponsor.partner_name"/> .</span>
@@ -74,7 +74,7 @@
<t t-if="not event.is_done">
<span>This room is not open right now!</span><br />
Join us here on the
<strong itemprop="startDate" t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'format': 'medium'}"/>
<strong itemprop="startDate" t-field="event.date_begin" t-options="{'tz_name': event.date_tz, 'format': 'medium'}"/>
<strong>(<t t-out="event.date_tz"/>)</strong>
to have a chat with us!
</t>
@@ -4,6 +4,7 @@
from werkzeug import urls
from odoo import api, fields, models, _
from odoo.tools import mute_logger
from odoo.tools.translate import html_translate
@@ -49,9 +50,9 @@ class Job(models.Model):
_name = 'hr.job'
_inherit = ['hr.job', 'website.seo.metadata', 'website.published.multi.mixin']
@mute_logger('odoo.addons.base.models.ir_qweb')
def _get_default_website_description(self):
default_description = self.env.ref("website_hr_recruitment.default_website_description", raise_if_not_found=False)
return (default_description._render() if default_description else "")
return self.env['ir.qweb']._render("website_hr_recruitment.default_website_description", raise_if_not_found=False)
website_published = fields.Boolean(help='Set if the application is published on the website of the company.')
website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description, prefetch=False, sanitize_form=False)
@@ -27,11 +27,11 @@ class PaymentTransaction(models.Model):
self.ensure_one()
if is_internal_notification or self.state == 'done':
subject = _('A donation has been made on your website') if is_internal_notification else _('Donation confirmation')
body = self.env.ref('website_payment.donation_mail_body')._render({
body = self.env['ir.qweb']._render('website_payment.donation_mail_body', {
'is_internal_notification': is_internal_notification,
'tx': self,
'comment': comment,
}, engine='ir.qweb', minimal_qcontext=True)
}, minimal_qcontext=True)
self.env.ref('website_payment.mail_template_donation').send_mail(
self.id,
email_layout_xmlid="mail.mail_notification_light",
@@ -30,11 +30,10 @@ class ProductWishlist(models.Model):
notified = self.env['product.wishlist']
# cannot group by product_id because it depend of website_id -> warehouse_id
tmpl = self.env.ref("website_sale_stock_wishlist.availability_email_body")
for wishlist in to_notify:
product = wishlist.with_context(website_id=wishlist.website_id.id).product_id
if not product._is_sold_out():
body_html = tmpl._render({"wishlist": wishlist})
body_html = self.env['ir.qweb']._render('website_sale_stock_wishlist.availability_email_body', {"wishlist": wishlist})
msg = self.env["mail.message"].sudo().new(dict(body=body_html, record_name=product.name))
full_mail = self.env["mail.render.mixin"]._render_encapsulate(
"mail.mail_notification_light",
+1 -1
View File
@@ -988,7 +988,7 @@ class WebsiteSlides(WebsiteProfile):
]).write({'completed': False})
slide_question = request.env['slide.question'].create(new_question_values)
return request.env.ref('website_slides.lesson_content_quiz_question')._render({
return request.env['ir.qweb']._render('website_slides.lesson_content_quiz_question', {
'slide': slide,
'question': slide_question,
})
@@ -84,20 +84,18 @@ class SlideChannelInvite(models.TransientModel):
# optional support of default_email_layout_xmlid in context
email_layout_xmlid = self.env.context.get('default_email_layout_xmlid', self.env.context.get('notif_layout'))
if email_layout_xmlid:
try:
template = self.env.ref(email_layout_xmlid, raise_if_not_found=True)
except ValueError:
_logger.warning('QWeb template %s not found when sending slide channel mails. Sending without layout.', email_layout_xmlid)
else:
# could be great to use ``_notify_by_email_prepare_rendering_context`` someday
template_ctx = {
'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.channel_id.name)),
'model_description': self.env['ir.model']._get('slide.channel').display_name,
'record': slide_channel_partner,
'company': self.env.company,
'signature': self.channel_id.user_id.signature,
}
body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
# could be great to use ``_notify_by_email_prepare_rendering_context`` someday
template_ctx = {
'message': self.env['mail.message'].sudo().new({'body': mail_values['body_html'], 'record_name': self.channel_id.name}),
'model_description': self.env['ir.model']._get('slide.channel').display_name,
'record': slide_channel_partner,
'company': self.env.company,
'signature': self.channel_id.user_id.signature,
}
body = self.env['ir.qweb']._render(email_layout_xmlid, template_ctx, engine='ir.qweb', minimal_qcontext=True, raise_if_not_found=False)
if body:
mail_values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
else:
_logger.warning('QWeb template %s not found when sending slide channel mails. Sending without layout.', email_layout_xmlid)
return mail_values
+11 -21
View File
@@ -362,10 +362,9 @@ class IrActionsReport(models.Model):
IrConfig = self.env['ir.config_parameter'].sudo()
# Return empty dictionary if 'web.minimal_layout' not found.
layout = self.env.ref('web.minimal_layout', False)
layout = self.env.ref('web.minimal_layout', raise_if_not_found=False)
if not layout:
return {}
layout = self.env['ir.ui.view'].browse(self.env['ir.ui.view'].get_view_id('web.minimal_layout'))
base_url = IrConfig.get_param('report.url') or layout.get_base_url()
root = lxml.html.fromstring(html)
@@ -391,15 +390,15 @@ class IrActionsReport(models.Model):
# Retrieve bodies
for node in root.xpath(match_klass.format('article')):
layout_with_lang = layout
# set context language to body language
IrQweb = self.env['ir.qweb']
if node.get('data-oe-lang'):
layout_with_lang = layout_with_lang.with_context(lang=node.get('data-oe-lang'))
body = layout_with_lang._render({
'subst': False,
'body': Markup(lxml.html.tostring(node, encoding='unicode')),
'base_url': base_url
})
IrQweb = IrQweb.with_context(lang=node.get('data-oe-lang'))
body = IrQweb._render(layout.id, {
'subst': False,
'body': Markup(lxml.html.tostring(node, encoding='unicode')),
'base_url': base_url
}, raise_if_not_found=False)
bodies.append(body)
if node.get('data-oe-model') == self.model:
res_ids.append(int(node.get('data-oe-id', 0)))
@@ -417,12 +416,12 @@ class IrActionsReport(models.Model):
if attribute[0].startswith('data-report-'):
specific_paperformat_args[attribute[0]] = attribute[1]
header = layout._render({
header = self.env['ir.qweb']._render(layout.id, {
'subst': True,
'body': Markup(lxml.html.tostring(header_node, encoding='unicode')),
'base_url': base_url
})
footer = layout._render({
footer = self.env['ir.qweb']._render(layout.id, {
'subst': True,
'body': Markup(lxml.html.tostring(footer_node, encoding='unicode')),
'base_url': base_url
@@ -608,23 +607,14 @@ class IrActionsReport(models.Model):
if values is None:
values = {}
context = dict(self.env.context, inherit_branding=False)
# Browse the user instead of using the sudo self.env.user
user = self.env['res.users'].browse(self.env.uid)
website = None
if request and hasattr(request, 'website'):
if request.website is not None:
website = request.website
context = dict(context, translatable=context.get('lang') != request.env['ir.http']._get_default_lang().code)
view_obj = self.env['ir.ui.view'].sudo().with_context(context)
view_obj = self.env['ir.ui.view'].with_context(inherit_branding=False)
values.update(
time=time,
context_timestamp=lambda t: fields.Datetime.context_timestamp(self.with_context(tz=user.tz), t),
user=user,
res_company=user.company_id,
website=website,
web_base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''),
)
return view_obj._render_template(template, values).encode()
+199 -132
View File
@@ -127,8 +127,8 @@ Activate the choosed debugger.
When dev mode is enabled this allows python developers to have access to the
state of variables being rendered. The code generated by the QWeb engine is
not accessible, only the variables (values, compile_options, self) can be
analyzed or the methods that called the QWeb rendering.
not accessible, only the variables (values, self) can be analyzed or the
methods that called the QWeb rendering.
``t-if``
~~~~~~~~
@@ -374,20 +374,26 @@ import token
import tokenize
import io
import textwrap
import werkzeug
import math
from markupsafe import Markup, escape
from collections.abc import Sized, Mapping
from itertools import count, chain
from lxml import etree
from psycopg2.extensions import TransactionRollbackError
from dateutil.relativedelta import relativedelta
from odoo import api, models, tools
from odoo.tools import pycompat, frozendict, SUPPORTED_DEBUGGER
from odoo.tools.safe_eval import check_values, assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST
from odoo.tools import config, safe_eval, pycompat, SUPPORTED_DEBUGGER
from odoo.tools.safe_eval import assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST
from odoo.tools.json import scriptsafe
from odoo.tools.misc import get_lang
from odoo.tools.image import image_data_uri
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools.profiler import QwebTracker
from odoo.exceptions import UserError
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS
@@ -447,25 +453,30 @@ def indent_code(code, level):
class QWebException(Exception):
""" Management of errors that raised when rendering a QWeb template.
"""
def __init__(self, message, qweb, options, template=None, path=None, code=None):
def __init__(self, message, qweb, template=None, path=None, code=None):
self.name = template
self.code = code if options and options.get('dev_mode') else None
self.code = code if qweb.env.context.get('dev_mode') else None
self.path = path
self.html = None
if template is not None and path and ':' not in path:
element = qweb._get_template(template, options)[0]
nodes = element.getroottree().xpath(self.path)
if nodes:
node = etree.Element(nodes[0].tag, nodes[0].attrib)
self.html = etree.tostring(node, encoding='unicode')
try:
element = qweb._get_template(template)[0]
nodes = element.getroottree().xpath(self.path)
if nodes:
node = etree.Element(nodes[0].tag, nodes[0].attrib)
self.html = etree.tostring(node, encoding='unicode')
except Exception as e:
_logger.warning('Cannot load template %s: %s', template, e)
self.stack = traceback.format_exc()
self.title = message
super().__init__(message)
def __str__(self):
parts = [self.title]
if self.__cause__ is not None:
if str(self.__cause__) != '':
parts.append(f"{self.__cause__.__class__.__name__}: {self.__cause__}")
elif str(self.__context__) != '':
parts.append(f"{self.__context__.__class__.__name__}: {self.__context__}")
if self.name is not None:
parts.append(f"Template: {self.name}")
if self.path is not None:
@@ -508,68 +519,99 @@ class IrQWeb(models.AbstractModel):
:param template: etree, xml_id, template name (see _get_template)
* Call the method ``load`` is not an etree.
:param dict values: template values to be used for rendering
:param options: used to compile the template (the dict available for the rendering is frozen)
:param options: used to compile the template
Options will be add into the IrQweb.env.context for the rendering.
* ``load`` (function) overrides the load method
* ``lang`` (str) used language to render the template
* ``inherit_branding`` (bool) add the tag node branding
* ``inherit_branding_auto`` (bool) add the branding on fields
* ``minimal_qcontext``(bool) To use the minimum context and options
from ``_prepare_environment``
:returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
instead of `str`)
:rtype: MarkupSafe
"""
compile_options = dict(self.env.context, dev_mode='qweb' in tools.config['dev_mode'])
compile_options.update(options)
if values and T_CALL_SLOT in values:
values = values.copy() if values else {}
if T_CALL_SLOT in values:
raise ValueError('values[0] should be unset when call the _render method and only set into the template.')
render_template = self._compile(template, compile_options)
rendering = render_template(self, values or {})
irQweb = self.with_context(**options)._prepare_environment(values)
safe_eval.check_values(values)
render_template = irQweb._compile(template)
rendering = render_template(irQweb, values)
result = ''.join(rendering)
return Markup(result)
# assume cache will be invalidated by third party on write to ir.ui.view
def _get_template_cache_keys(self):
""" Return the list of context keys to use for caching ``_get_template``. """
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id', 'profile']
""" Return the list of context keys to use for caching ``_compile``. """
return ['lang', 'inherit_branding', 'edit_translations', 'profile']
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache('template', 'tuple(options.get(k) for k in self._get_template_cache_keys())'),
tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
)
@QwebTracker.wrap_compile
def _compile(self, template, options):
def _compile(self, template):
""" Compile the given template into a rendering function (generator)::
render(qweb, values)
This method can be called only by the IrQweb `_render` method or by
the compiled code of t-call from an other template.
where ``qweb`` is a QWeb instance and ``values`` are the values to render.
An `options` dictionary is created and attached to the function. It
contains rendering options that are part of the cache key in
addition to template references.
where ``qweb`` is a QWeb instance and ``values`` are the values to
render.
"""
if options is None:
options = {}
# The `context`` dictionary includes the elements used for the cache
# key to which are added the template references as well as technical
# information useful for generating the function. This dictionary is
# only used when compiling the template.
context = self.env.context.copy()
element, document, ref = self._get_template(template, options)
element.attrib.pop('t-name', None)
try:
element, document, ref = self._get_template(template)
except (ValueError, UserError) as e:
message = str(e)
ClassError = e.__class__
def not_found_template(self, values):
if self.env.context.get('raise_if_not_found', True):
raise ClassError(message)
_logger.warning('Cannot load template %s: %s', template, message)
return ''
return not_found_template
context.pop('raise_if_not_found', None)
# reference to get xml and etree (usually the template ID)
options['ref'] = ref
context['ref'] = ref
# reference name or key to get xml and etree (usually the template XML ID)
context['ref_name'] = element.attrib.pop('t-name', template if isinstance(template, str) and '<' not in template else None)
# str xml of the reference template used for compilation. Useful for debugging, dev mode and profiling.
options['ref_xml'] = document
_options = dict(options)
options = frozendict(options)
# Initial template value send to render method (not in the froozen dict because it may be
# different from one render to another. Indeed, it may be the view ID or the key)
_options['template'] = template
context['ref_xml'] = document
# Identifier used to call `_compile`
context['template'] = template
# Root of the etree which will be processed during compilation.
_options['root'] = element.getroottree()
# Reference to the last node being compiled. It is mainly used for debugging and displaying
# error messages.
_options['last_path_node'] = None
context['root'] = element.getroottree()
# Reference to the last node being compiled. It is mainly used for debugging and displaying error messages.
context['last_path_node'] = None
if not options.get('nsmap'):
_options['nsmap'] = {}
if not context.get('nsmap'):
context['nsmap'] = {}
# The options dictionary includes cache key elements and template
# references. It will be attached to the generated function. This
# dictionary is only there for logs, performance or test information.
# The values of these `options` cannot be changed and must always be
# identical in `context` and `self.env.context`.
options = {k: context.get(k) for k in self._get_template_cache_keys() + ['ref', 'ref_name', 'ref_xml']}
# generate code
@@ -579,24 +621,24 @@ class IrQWeb(models.AbstractModel):
if element.text:
element.text = re.compile(r'^(\n[ \t]*)+(\n[ \t])').sub(r'\2', element.text)
_options['_text_concat'] = []
self._append_text("", _options) # To ensure the template function is a generator and doesn't become a regular function
context['_text_concat'] = []
self._append_text("", context) # To ensure the template function is a generator and doesn't become a regular function
code_lines = (
[f'def {def_name}(self, compile_options, values, log):']
+ self._compile_node(element, _options, 1)
+ self._flush_text(_options, 1, rstrip=True)
[f'def {def_name}(self, values, log):']
+ self._compile_node(element, context, 1)
+ self._flush_text(context, 1, rstrip=True)
)
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, options, template, path=_options.get('last_path_node')) from e
self, template, path=context.get('last_path_node')) from e
try:
code = '\n'.join(code_lines)
except Exception as e:
code = '\n'.join(map(str, code_lines))
raise QWebException("Error when compiling xml template",
self, options, template, code=code) from e
self, template, code=code) from e
# compile code and defined default values
@@ -611,35 +653,38 @@ class IrQWeb(models.AbstractModel):
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, options, template, code=code) from e
self, template, code=code) from e
# return the wrapped function
def render_template(self, values):
try:
if not values.get('xmlid'):
values['xmlid'] = context['ref_name']
values['viewid'] = context['ref']
log = {'last_path_node': None}
values = self._prepare_values(values, options)
yield from compiled_fn(self, options, values, log)
yield from compiled_fn(self, values, log)
except (QWebException, TransactionRollbackError) as e:
raise
except Exception as e:
raise QWebException("Error while render the template",
self, options, template, path=log.get('last_path_node'), code=code) from e
self, template, path=log.get('last_path_node'), code=code) from e
# add key cache options for profiling tools
render_template.options = options
return render_template
# read and load input template
def _get_template(self, template, options):
def _get_template(self, template):
""" Retrieve the given template, and return it as a tuple ``(etree,
xml, ref)``, where ``element`` is an etree, ``document`` is the
string document that contains ``element``, and ``ref`` if the uniq
reference of the template (id, t-name or template).
:param template: template identifier or etree
:param options: used to compile the template (the dict available for
the rendering is frozen)
``load`` (function) overrides the load method
:param self.env.context['load']: (function(template)) overrides the load method
"""
assert template not in (False, None, ""), "template is required"
@@ -659,7 +704,7 @@ class IrQWeb(models.AbstractModel):
except ValueError:
ref_alias = template # e.g. web.layout
doc_or_elem, ref = options.get('load', self._load)(ref_alias, options) or (None, None)
doc_or_elem, ref = self.env.context.get('load', self._load)(ref_alias) or (None, None)
if doc_or_elem is None:
raise ValueError(f"Can not load template: {ref_alias!r}")
if isinstance(doc_or_elem, etree._Element):
@@ -687,53 +732,71 @@ class IrQWeb(models.AbstractModel):
# use the document itself as ref when no t-name was found
return (element, document, document)
def _load(self, ref, options):
def _load(self, ref):
"""
Load the template referenced by ``ref``.
:returns: The loaded template (as string or etree) and its
identifier
:rtype: Optional[Tuple[Union[etree, str], Optional[str, int]]]
:rtype: Tuple[Union[etree, str], Optional[str, int]]
"""
lang = options.get('lang') or get_lang(self.env).code
view_id = self.env['ir.ui.view'].get_view_id(ref)
template = self.env['ir.ui.view'].with_context(lang=lang).sudo()._read_template(view_id)
IrUIView = self.env['ir.ui.view']
view_id = IrUIView.get_view_id(ref)
view = IrUIView.sudo().browse(view_id)
template = IrUIView.sudo()._read_template(view_id)
etree_view = etree.fromstring(template)
# QWeb's ``_read_template`` will check if one of the first children of
# what we send to it has a "t-name" attribute having ``ref`` as value
# to consider it has found it. As it'll never be the case when working
# with view ids or children view or children primary views, force it here.
def is_child_view(view_ref):
view_id = self.env['ir.ui.view'].get_view_id(view_ref)
view = self.env['ir.ui.view'].sudo().browse(view_id)
return view.inherit_id is not None
if isinstance(ref, int) or is_child_view(ref):
view = etree.fromstring(template)
for node in view:
if node.get('t-name'):
node.set('t-name', str(ref))
if view.inherit_id is not None:
for node in etree_view:
if node.get('t-name') == str(ref) or node.get('t-name') == str(view.key):
node.attrib.pop('name', None)
node.attrib.pop('id', None)
return (view, view_id)
else:
return (template, view_id)
etree_view = node
break
etree_view.set('t-name', str(view.key or ref))
return (etree_view, view_id)
# values for running time
def _prepare_values(self, values, options):
""" Prepare the context that will sent to the compiled and evaluated
function.
def _prepare_environment(self, values):
""" Prepare the values and options that will sent to the
compiled and evaluated function.
The options will be add into the rendering IrQweb.env.context.
:param values: template values to be used for rendering
:param options: frozen dict of compilation parameters.
:returns self
"""
check_values(values)
values['true'] = True
values['false'] = False
if 'request' not in values:
values['request'] = request
return values
values.update(
true=True,
false=False,
)
if not self.env.context.get('minimal_qcontext'):
values.setdefault('debug', request and request.session.debug or '')
values.setdefault('user_id', self.env["res.users"].browse(self.env.user.id))
values.setdefault('res_company', self.env.company.sudo())
values.update(
request=request, # might be unbound if we're not in an httprequest context
test_mode_enabled=bool(config['test_enable'] or config['test_file']),
json=scriptsafe,
quote_plus=werkzeug.urls.url_quote_plus,
time=safe_eval.time,
datetime=safe_eval.datetime,
relativedelta=relativedelta,
image_data_uri=image_data_uri,
# specific 'math' functions to ease rounding in templates and lessen controller marshmalling
floor=math.floor,
ceil=math.ceil,
env=self.env,
lang=self.env.context.get('lang'),
)
return self.with_context(dev_mode='qweb' in tools.config['dev_mode'])
def _prepare_globals(self):
""" Prepare the global context that will sent to eval the qweb
@@ -1119,7 +1182,7 @@ class IrQWeb(models.AbstractModel):
""" Compile a purely static element into a list of string. """
if not el.nsmap:
unqualified_el_tag = el_tag = el.tag
attrib = self._post_processing_att(el.tag, el.attrib, options)
attrib = self._post_processing_att(el.tag, el.attrib)
else:
# Etree will remove the ns prefixes indirection by inlining the corresponding
# nsmap definition into the tag attribute. Restore the tag and prefix here.
@@ -1149,7 +1212,7 @@ class IrQWeb(models.AbstractModel):
else:
attrib[key] = value
attrib = self._post_processing_att(el.tag, attrib, options)
attrib = self._post_processing_att(el.tag, attrib)
# Update the dict of inherited namespaces before continuing the recursion. Note:
# since `options['nsmap']` is a dict (and therefore mutable) and we do **not**
@@ -1246,7 +1309,7 @@ class IrQWeb(models.AbstractModel):
debugger = el.attrib.pop('t-debug')
code = []
if options.get('dev_mode'):
code.append(indent_code(f"self._debug_trace({debugger!r}, compile_options)", level))
code.append(indent_code(f"self._debug_trace({debugger!r})", level))
else:
_logger.warning("@t-debug in template is only available in qweb dev mode options")
return code
@@ -1390,7 +1453,7 @@ class IrQWeb(models.AbstractModel):
attrs = values.pop('__qweb_attrs__', None)
if attrs:
tagName = {el.tag!r}
attrs = self._post_processing_att(tagName, attrs, compile_options)
attrs = self._post_processing_att(tagName, attrs)
for name, value in attrs.items():
if value or isinstance(value, str):
yield f' {{escape(str(name))}}="{{escape(str(value))}}"'
@@ -1454,9 +1517,9 @@ class IrQWeb(models.AbstractModel):
self._compile_directive(el, options, 'inner-content', level + 1) +
self._flush_text(options, level + 1))
if content:
code.append(indent_code(f"def {def_name}(self, compile_options, values, log):", level))
code.append(indent_code(f"def {def_name}(self, values, log):", level))
code.extend(content)
expr = f"Markup(''.join({def_name}(self, compile_options, values, log)))"
expr = f"Markup(''.join({def_name}(self, values, log)))"
else:
expr = "''"
code.append(indent_code(f"values[{varname!r}] = {expr}", level))
@@ -1777,7 +1840,7 @@ class IrQWeb(models.AbstractModel):
elif ttype == 't-field':
record, field_name = expr.rsplit('.', 1)
code.append(indent_code(f"""
field_attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {field_name!r}, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), compile_options, values)
field_attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {field_name!r}, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), values)
if values.get('__qweb_attrs__') is None:
values['__qweb_attrs__'] = field_attrs
else:
@@ -1795,7 +1858,7 @@ class IrQWeb(models.AbstractModel):
if code_options == 'True':
code.append(indent_code(f"""
widget_attrs, content, force_display = self._get_widget(content, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), compile_options, values)
widget_attrs, content, force_display = self._get_widget(content, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), values)
if values.get('__qweb_attrs__') is None:
values['__qweb_attrs__'] = widget_attrs
else:
@@ -1921,9 +1984,9 @@ class IrQWeb(models.AbstractModel):
code = self._flush_text(options, level, rstrip=el.tag.lower() == 't')
# options
el.attrib.pop('t-consumed-options', None)
code.append(indent_code(f"""
t_options = values.pop('__qweb_options__', {{}})
t_call_options = compile_options.copy()
t_call_options = dict(values.pop('__qweb_options__', {{}}))
t_call_options.update({{'caller_template': {str(options.get('template'))!r}, 'last_path_node': {str(options['root'].getpath(el))!r} }})
""", level))
if nsmap:
@@ -1939,28 +2002,22 @@ class IrQWeb(models.AbstractModel):
# values (t-out="0" from content and variables from t-set and t-set-*)
def_name = self._make_name("t_call_values")
code.append(indent_code(f"def {def_name}(self, compile_options, values, log):", level))
code.append(indent_code(f"def {def_name}(self, values, log):", level))
code.extend(self._compile_directive(el, options, 'inner-content', level + 1))
code.extend(self._compile_directive(el, options, 'set', level + 1))
self._append_text('', options) # To ensure the template function is a generator and doesn't become a regular function
code.extend(self._flush_text(options, level + 1, rstrip=True))
code.append(indent_code("t_call_values = values.copy()", level))
code.append(indent_code(f"t_call_values['0'] = Markup(''.join({def_name}(self, compile_options, t_call_values, log)))", level))
code.append(indent_code(f"t_call_values['0'] = Markup(''.join({def_name}(self, t_call_values, log)))", level))
template = self._compile_format(expr)
# call
if el.attrib.pop('t-consumed-options', 'None') == 'True':
code.append(indent_code("t_call_options.update(t_options)", level))
code.append(indent_code(f"""
if compile_options.get('lang') != t_call_options.get('lang'):
self_lang = self.with_context(lang=t_call_options.get('lang'))
yield from self_lang._compile({template}, t_call_options)(self_lang, t_call_values)
else:
yield from self._compile({template}, t_call_options)(self, t_call_values)
""", level))
else:
code.append(indent_code(f"yield from self._compile({template}, t_call_options)(self, t_call_values)", level))
code.append(indent_code(f"""
irQweb = self.with_context(**t_call_options)
render_template = irQweb._compile({template})
yield from render_template(irQweb, t_call_values)
""", level))
return code
@@ -2003,7 +2060,7 @@ class IrQWeb(models.AbstractModel):
yield '<'
yield tagName
attrs = self._post_processing_att(tagName, asset_attrs, compile_options)
attrs = self._post_processing_att(tagName, asset_attrs)
for name, value in attrs.items():
if value or isinstance(value, str):
yield f' {escape(str(name))}="{escape(str(value))}"'
@@ -2023,14 +2080,14 @@ class IrQWeb(models.AbstractModel):
# methods called by the compiled function at rendering time.
def _debug_trace(self, debugger, options):
def _debug_trace(self, debugger):
"""Method called at compile time to load debugger."""
if debugger in SUPPORTED_DEBUGGER:
__import__(debugger).set_trace()
else:
raise ValueError(f"unsupported t-debug value: {debugger}")
def _post_processing_att(self, tagName, atts, options):
def _post_processing_att(self, tagName, atts):
""" Method called at compile time for the static node and called at
runing time for the dynamic attributes.
@@ -2042,7 +2099,7 @@ class IrQWeb(models.AbstractModel):
"""
return atts
def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
def _get_field(self, record, field_name, expression, tagName, field_options, values):
"""Method called at compile time to return the field value.
:returns: tuple:
@@ -2052,16 +2109,16 @@ class IrQWeb(models.AbstractModel):
"""
field = record._fields[field_name]
# adds template compile options for rendering fields
field_options['template_options'] = options
# adds generic field options
field_options['tagName'] = tagName
field_options['expression'] = expression
field_options['type'] = field_options.get('widget', field.type)
inherit_branding = options.get('inherit_branding', options.get('inherit_branding_auto') and record.check_access_rights('write', False))
inherit_branding = (
self.env.context['inherit_branding']
if 'inherit_branding' in self.env.context
else self.env.context.get('inherit_branding_auto') and record.check_access_rights('write', False))
field_options['inherit_branding'] = inherit_branding
translate = options.get('edit_translations') and options.get('translatable') and field.translate
translate = self.env.context.get('edit_translations') and values.get('translatable') and field.translate
field_options['translate'] = translate
# field converter
@@ -2074,7 +2131,7 @@ class IrQWeb(models.AbstractModel):
return (attributes, content, inherit_branding or translate)
def _get_widget(self, value, expression, tagName, field_options, options, values):
def _get_widget(self, value, expression, tagName, field_options, values):
"""Method called at compile time to return the widget value.
:returns: tuple:
@@ -2082,12 +2139,11 @@ class IrQWeb(models.AbstractModel):
* string or None: content
* boolean: force_display display the tag if the content and default_content are None
"""
# adds template compile options for rendering fields
field_options['template_options'] = options
field_options['type'] = field_options['widget']
field_options['tagName'] = tagName
field_options['expression'] = expression
inherit_branding = self.env.context.get('inherit_branding')
field_options['inherit_branding'] = inherit_branding
# field converter
model = 'ir.qweb.field.' + field_options['type']
@@ -2099,7 +2155,7 @@ class IrQWeb(models.AbstractModel):
attributes['data-oe-type'] = field_options['type']
attributes['data-oe-expression'] = field_options['expression']
return (attributes, content, None)
return (attributes, content, inherit_branding)
def _get_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
"""Generates asset nodes.
@@ -2185,11 +2241,10 @@ def render(template_name, values, load, **options):
(Widget, field, or asset rendering is not implemented.)
:param (string|int) template_name: template identifier
:param dict values: template values to be used for rendering
:param def load: function like `load(template_name, options)` which
returns an etree from the given template name (from initial rendering
or template `t-call`).
:param options: used to compile the template (the dict available for the
rendering is frozen)
:param def load: function like `load(template_name)` which returns an etree
from the given template name (from initial rendering or template
`t-call`).
:param options: used to compile the template
:returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
instead of `str`)
:rtype: MarkupSafe
@@ -2203,6 +2258,11 @@ def render(template_name, values, load, **options):
pool = MockPool()
def _prepare_environment(self, values):
values['true'] = True
values['false'] = False
return self
def _get_field(self, *args):
raise NotImplementedError("Fields are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
@@ -2217,6 +2277,13 @@ def render(template_name, values, load, **options):
super().__init__()
self.context = {}
renderer = object.__new__(MockIrQWeb)
renderer.env = MockEnv()
return renderer._render(template_name, values, load=load, **options)
def __call__(self, cr=None, user=None, context=None, su=None):
""" Return an mocked environment based and update the sent context.
Allow to use `ir_qweb.with_context` with sand boxed qweb.
"""
env = MockEnv()
env.context.update(self.context if context is None else context)
return env
renderer = MockIrQWeb(MockEnv(), tuple(), tuple())
return renderer._render(template_name, values, load=load, minimal_qcontext=True, **options)
+9 -10
View File
@@ -117,8 +117,8 @@ class FieldConverter(models.AbstractModel):
"""
if not record:
return False
value = record[field_name]
return False if value is False else record.env[self._name].value_to_html(value, options=options)
value = record.with_context(**self.env.context)[field_name]
return False if value is False else self.value_to_html(value, options=options)
@api.model
def user_lang(self):
@@ -236,7 +236,6 @@ class DateTimeConverter(models.AbstractModel):
def value_to_html(self, value, options):
if not value:
return ''
options = options or {}
lang = self.user_lang()
locale = babel_locale_parse(lang.code)
@@ -244,13 +243,14 @@ class DateTimeConverter(models.AbstractModel):
if isinstance(value, str):
value = fields.Datetime.from_string(value)
value = fields.Datetime.context_timestamp(self, value)
if options.get('tz_name'):
self = self.with_context(tz=options['tz_name'])
tzinfo = babel.dates.get_timezone(options['tz_name'])
else:
tzinfo = None
value = fields.Datetime.context_timestamp(self, value)
if 'format' in options:
pattern = options['format']
else:
@@ -357,7 +357,7 @@ class HTMLConverter(models.AbstractModel):
for element in body.iter():
if element.attrib:
attrib = dict(element.attrib)
attrib = irQweb._post_processing_att(element.tag, attrib, options.get('template_options'))
attrib = irQweb._post_processing_att(element.tag, attrib)
element.attrib.clear()
element.attrib.update(attrib)
return M(etree.tostring(body, encoding='unicode', method='html')[6:-7])
@@ -750,10 +750,9 @@ class Contact(models.AbstractModel):
opf = options.get('fields') or ["name", "address", "phone", "mobile", "email"]
sep = options.get('separator')
template_options = options.get('template_options', {})
if sep:
opsep = escape(sep)
elif template_options.get('no_tag_br'):
elif options.get('no_tag_br'):
# escaped joiners will auto-escape joined params
opsep = escape(', ')
else:
@@ -783,7 +782,7 @@ class Contact(models.AbstractModel):
'object': value,
'options': options
}
return self.env['ir.qweb']._render('base.contact', val, **template_options)
return self.env['ir.qweb']._render('base.contact', val, minimal_qcontext=True)
class QwebView(models.AbstractModel):
@@ -801,4 +800,4 @@ class QwebView(models.AbstractModel):
_logger.warning("%s.%s must be a 'ir.ui.view', got %r.", record, field_name, view._name)
return ''
return view._render(options.get('values', {}), engine='ir.qweb')
return self.env['ir.qweb']._render(view.id, options.get('values', {}))
+4 -68
View File
@@ -15,9 +15,6 @@ import time
import uuid
import warnings
from dateutil.relativedelta import relativedelta
import werkzeug, werkzeug.urls
from lxml import etree
from lxml.etree import LxmlError
from lxml.builder import E
@@ -28,11 +25,9 @@ from odoo.http import request
from odoo.modules.module import get_resource_from_path, get_resource_path
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 import safe_eval, lazy_property, frozendict
from odoo.tools.view_validation import valid_view, get_variable_names, get_domain_identifiers, get_dict_asts
from odoo.tools.translate import xml_translate, TRANSLATED_ATTRS
from odoo.tools.image import image_data_uri
from odoo.models import check_method_name
from odoo.osv.expression import expression
@@ -122,29 +117,6 @@ def transfer_modifiers_to_node(modifiers, node):
node.set('modifiers', json.dumps(modifiers))
def keep_query(*keep_params, **additional_params):
"""
Generate a query string keeping the current request querystring's parameters specified
in ``keep_params`` and also adds the parameters specified in ``additional_params``.
Multiple values query string params will be merged into a single one with comma seperated
values.
The ``keep_params`` arguments can use wildcards too, eg:
keep_query('search', 'shop_*', page=4)
"""
if not keep_params and not additional_params:
keep_params = ('*',)
params = additional_params.copy()
qs_keys = list(request.httprequest.args) if request else []
for keep_param in keep_params:
for param in fnmatch.filter(qs_keys, keep_param):
if param not in additional_params and param in qs_keys:
params[param] = request.httprequest.args.getlist(param)
return werkzeug.urls.url_encode(params)
class ViewCustom(models.Model):
_name = 'ir.ui.view.custom'
_description = 'Custom View'
@@ -1842,7 +1814,7 @@ actual arch.
def _read_template_keys(self):
""" Return the list of context keys to use for caching ``_read_template``. """
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
return ['lang', 'inherit_branding', 'edit_translations']
# apply ormcache_context decorator unless in dev mode...
@api.model
@@ -1963,46 +1935,10 @@ actual arch.
def render_public_asset(self, template, values=None):
template = self.sudo().browse(self.get_view_id(template))
template._check_view_access()
return template.sudo()._render(values, engine="ir.qweb")
return self.env['ir.qweb'].sudo()._render(template, values)
def _render_template(self, template, values=None, engine='ir.qweb'):
return self.browse(self.get_view_id(template))._render(values, engine)
def _render(self, values=None, engine='ir.qweb', minimal_qcontext=False, options=None):
assert isinstance(self.id, int)
qcontext = dict() if minimal_qcontext else self._prepare_qcontext()
qcontext.update(values or {})
return self.env[engine]._render(self.id, qcontext, **(options or {}))
@api.model
def _prepare_qcontext(self):
""" Returns the qcontext : rendering context with website specific value (required
to render website layout template)
"""
qcontext = dict(
env=self.env,
user_id=self.env["res.users"].browse(self.env.user.id),
res_company=self.env.company.sudo(),
keep_query=keep_query,
request=request, # might be unbound if we're not in an httprequest context
debug=request.session.debug if request else '',
test_mode_enabled=bool(config['test_enable'] or config['test_file']),
json=json_scriptsafe,
quote_plus=werkzeug.urls.url_quote_plus,
time=safe_eval.time,
datetime=safe_eval.datetime,
relativedelta=relativedelta,
xmlid=self.sudo().key,
viewid=self.id,
to_text=pycompat.to_text,
image_data_uri=image_data_uri,
# specific 'math' functions to ease rounding in templates and lessen controller marshmalling
floor=math.floor,
ceil=math.ceil,
)
return qcontext
def _render_template(self, template, values=None):
return self.env['ir.qweb']._render(template, values)
#------------------------------------------------------
# Misc
+2
View File
@@ -461,6 +461,7 @@ class TestProfiling(TransactionCase):
template = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'key': 'root',
'arch_db': '''<t t-name="root">
<t t-foreach="{'a': 3, 'b': 2, 'c': 1}" t-as="item">
[<t t-esc="item_index"/>: <t t-call="base.dummy" t-set-record="item"/> <t t-esc="item_value"/>]
@@ -470,6 +471,7 @@ class TestProfiling(TransactionCase):
child_template = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'key': 'dummy',
'arch_db': '<t t-name="dummy"><span t-attf-class="myclass"><t t-esc="record"/> <t t-esc="add_one_query()"/></span></t>'
})
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
+141 -43
View File
@@ -17,7 +17,7 @@ from odoo.tests.common import TransactionCase
from odoo.addons.base.models.ir_qweb import QWebException, render
from odoo.tools import misc, mute_logger
from odoo.tools.json import scriptsafe as json_scriptsafe
from odoo.exceptions import MissingError
from odoo.exceptions import UserError, ValidationError, MissingError
unsafe_eval = eval
@@ -79,7 +79,7 @@ class TestQWebTField(TransactionCase):
<t t-name="base.dummy"><root><span t-esc="5" t-options="{'widget': 'char'}" t-options-widget="'float'" t-options-precision="4"/></root></t>
"""
})
text = etree.fromstring(view1._render()).find('span').text
text = etree.fromstring(self.env['ir.qweb']._render(view1.id)).find('span').text
self.assertEqual(text, '5.0000')
def test_xss_breakout(self):
@@ -95,7 +95,7 @@ class TestQWebTField(TransactionCase):
</t>
"""
})
rendered = view._render({'malicious': '1</script><script>alert("pwned")</script><script>'})
rendered = self.env['ir.qweb']._render(view.id, {'malicious': '1</script><script>alert("pwned")</script><script>'})
self.assertIn('alert', rendered, "%r doesn't seem to be rendered" % rendered)
doc = etree.fromstring(rendered)
self.assertEqual(len(doc.xpath('//script')), 1)
@@ -126,7 +126,7 @@ class TestQWebNS(TransactionCase):
""" % expected_result
})
self.assertEqual(etree.fromstring(view1._render()), etree.fromstring(expected_result))
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), etree.fromstring(expected_result))
def test_render_static_xml_with_namespace_2(self):
""" Test the rendering on a namespaced view with no static content. The resulting string should be untouched.
@@ -164,7 +164,7 @@ class TestQWebNS(TransactionCase):
""" % expected_result
})
self.assertEqual(etree.fromstring(view1._render()), etree.fromstring(expected_result))
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), etree.fromstring(expected_result))
def test_render_static_xml_with_useless_distributed_namespace(self):
""" Test that redundant namespaces are stripped upon rendering.
@@ -197,7 +197,7 @@ class TestQWebNS(TransactionCase):
</root>
""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_static_xml_with_namespace_3(self):
expected_result = """
@@ -212,7 +212,7 @@ class TestQWebNS(TransactionCase):
""" % expected_result
})
self.assertEqual(etree.fromstring(view1._render()), etree.fromstring(expected_result))
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), etree.fromstring(expected_result))
def test_render_static_xml_with_namespace_dynamic(self):
""" Test the rendering on a namespaced view with dynamic URI (need default namespace uri).
@@ -248,7 +248,7 @@ class TestQWebNS(TransactionCase):
""" % tempate
})
rendering = view1._render(values, engine='ir.qweb')
rendering = self.env['ir.qweb']._render(view1.id, values)
self.assertEqual(etree.fromstring(rendering), etree.fromstring(expected_result % values))
@@ -293,7 +293,7 @@ class TestQWebNS(TransactionCase):
""" % tempate
})
rendering = view1._render(values, engine='ir.qweb')
rendering = self.env['ir.qweb']._render(view1.id, values)
self.assertEqual(etree.fromstring(rendering), etree.fromstring(expected_result % values))
@@ -313,7 +313,7 @@ class TestQWebNS(TransactionCase):
expected_result = etree.fromstring("""<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">test</Invoice>""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_dynamic_xml_with_namespace_t_esc_with_useless_distributed_namespace(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-esc attribute correctly
@@ -337,7 +337,7 @@ class TestQWebNS(TransactionCase):
</Invoice>
""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_dynamic_xml_with_namespace_t_attf(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-attf attribute correctly
@@ -377,7 +377,7 @@ class TestQWebNS(TransactionCase):
</root>
""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_dynamic_xml_with_namespace_t_attf_with_useless_distributed_namespace(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-attf attribute correctly
@@ -419,7 +419,7 @@ class TestQWebNS(TransactionCase):
""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_dynamic_xml_with_namespace_2(self):
view1 = self.env['ir.ui.view'].create({
@@ -452,7 +452,7 @@ class TestQWebNS(TransactionCase):
</Invoice>
""")
self.assertEqual(etree.fromstring(view1._render({'version_id': 1.0})), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id, {'version_id': 1.0})), expected_result)
def test_render_static_xml_with_namespaced_attributes(self):
view1 = self.env['ir.ui.view'].create({
@@ -467,7 +467,7 @@ class TestQWebNS(TransactionCase):
expected_result = etree.fromstring("""<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_dynamic_xml_with_namespaced_attributes(self):
view1 = self.env['ir.ui.view'].create({
@@ -482,7 +482,7 @@ class TestQWebNS(TransactionCase):
expected_result = etree.fromstring("""<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>""")
self.assertEqual(etree.fromstring(view1._render()), expected_result)
self.assertEqual(etree.fromstring(self.env['ir.qweb']._render(view1.id)), expected_result)
def test_render_static_xml_with_t_call(self):
view1 = self.env['ir.ui.view'].create({
@@ -516,7 +516,7 @@ class TestQWebNS(TransactionCase):
"""
})
result = view2._render()
result = self.env['ir.qweb']._render(view2.id)
result_etree = etree.fromstring(result)
# check that the root tag has all its xmlns
@@ -580,7 +580,7 @@ class TestQWebNS(TransactionCase):
""")
self.assertEqual(
etree.fromstring(view1.with_context(check_view_ids=[view1.id, view2.id])._render()),
etree.fromstring(self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id])._render(view1.id)),
expected_result
)
@@ -606,7 +606,7 @@ class TestQWebNS(TransactionCase):
error_msg = e.args[0]
with self.assertRaises(QWebException, msg=error_msg):
view1._render()
self.env['ir.qweb']._render(view1.id)
class TestQWebBasic(TransactionCase):
def test_compile_expr(self):
@@ -667,10 +667,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
t._render()
self.env['ir.qweb']._render(t.id)
try:
t._render()
self.env['ir.qweb']._render(t.id)
except QWebException as e:
error = str(e)
self.assertIn("KeyError: 't-as'", error)
@@ -687,10 +687,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
t._render()
self.env['ir.qweb']._render(t.id)
try:
t._render()
self.env['ir.qweb']._render(t.id)
except QWebException as e:
error = str(e)
self.assertIn("KeyError: 't-as'", error)
@@ -707,10 +707,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
t._render()
self.env['ir.qweb']._render(t.id)
try:
t._render()
self.env['ir.qweb']._render(t.id)
except QWebException as e:
error = str(e)
self.assertIn("The varname 'b-2' can only contain alphanumeric characters and underscores", error)
@@ -970,10 +970,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
t._render()
self.env['ir.qweb']._render(t.id)
try:
t._render()
self.env['ir.qweb']._render(t.id)
except QWebException as e:
error = str(e)
self.assertIn("KeyError: 't-set'", error)
@@ -989,10 +989,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
t._render()
self.env['ir.qweb']._render(t.id)
try:
t._render()
self.env['ir.qweb']._render(t.id)
except QWebException as e:
error = str(e)
self.assertIn("The varname can only contain alphanumeric characters and underscores", error)
@@ -1114,7 +1114,7 @@ class TestQWebBasic(TransactionCase):
<t t-name="base.dummy"><root><span t-out="text" t-options-widget="'text'"/></root></t>
"""
})
html = view1._render({'text': """a
html = self.env['ir.qweb']._render(view1.id, {'text': """a
b <b>c</b>"""})
self.assertEqual(html, """<root><span data-oe-type="text" data-oe-expression="text">a<br>
b &lt;b&gt;c&lt;/b&gt;</span></root>""")
@@ -1286,6 +1286,104 @@ class TestQWebBasic(TransactionCase):
except AssertionError as e:
self.assertIn('template is required', str(e))
def test_error_message_5(self):
# Error not found a first rendering.
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name="test">
<section>
<div t-esc="abc + def + (">
<span>content</span>
</div>
</section>
</t>'''
})
def load(ref_alias):
raise ValueError(f'Not Found: {ref_alias}')
with self.assertRaises(ValueError, msg=f"Not Found: {t.id}"):
self.env['ir.qweb']._render(t.id, load=load)
@mute_logger('odoo.addons.base.models.ir_qweb') # warning for template not found
def test_error_message_6(self):
# Error not found a second rendering (first rendering with option hide this error).
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name="test">
<section>
<div t-esc="abc + def + (">
<span>content</span>
</div>
</section>
</t>'''
})
def load(ref_alias):
raise ValueError(f'Not Found: {ref_alias}')
html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering without any error (use cached method)
html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering but raise (use cached method)
with self.assertRaises(ValueError, msg=f"Not Found: {t.id}"):
self.env['ir.qweb']._render(t.id, load=load)
def test_error_message_7(self):
# UserError not found a first rendering.
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name="test">
<section>
<div t-esc="abc + def + (">
<span>content</span>
</div>
</section>
</t>'''
})
def load(ref_alias):
raise ValidationError(f'Not Found: {ref_alias}')
with self.assertRaises(UserError, msg=f"Not Found: {t.id}"):
self.env['ir.qweb']._render(t.id, load=load)
@mute_logger('odoo.addons.base.models.ir_qweb') # warning for template not found
def test_error_message_8(self):
# UserError not found a second rendering (first rendering with option hide this error).
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'arch_db': '''<t t-name="test">
<section>
<div t-esc="abc + def + (">
<span>content</span>
</div>
</section>
</t>'''
})
def load(ref_alias):
raise ValidationError(f'Not Found: {ref_alias}')
html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering without any error (use cached method)
html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering but raise (use cached method)
with self.assertRaises(UserError, msg=f"Not Found: {t.id}"):
self.env['ir.qweb']._render(t.id, load=load)
def test_call_set(self):
view0 = self.env['ir.ui.view'].create({
'name': "dummy",
@@ -1320,7 +1418,7 @@ class TestQWebBasic(TransactionCase):
"""
})
result = view1._render({})
result = self.env['ir.qweb']._render(view1.id, {})
self.assertEqual(etree.fromstring(result), etree.fromstring("""
<div>
<table>
@@ -1359,7 +1457,7 @@ class TestQWebBasic(TransactionCase):
"""
})
result = view1._render({})
result = self.env['ir.qweb']._render(view1.id, {})
self.assertEqual(etree.fromstring(result), etree.fromstring("""
<div>
<table>
@@ -1398,10 +1496,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
view1._render()
self.env['ir.qweb']._render(view1.id)
try:
view1._render()
self.env['ir.qweb']._render(view1.id)
except QWebException as e:
error = str(e)
self.assertIn("The varname 'a-2' can only contain alphanumeric characters and underscores", error)
@@ -1421,10 +1519,10 @@ class TestQWebBasic(TransactionCase):
})
with self.assertRaises(QWebException):
view1._render()
self.env['ir.qweb']._render(view1.id)
try:
view1._render()
self.env['ir.qweb']._render(view1.id)
except QWebException as e:
error = str(e)
self.assertIn('External ID not found in the system: base.dummy', error)
@@ -1466,7 +1564,7 @@ class TestQWebBasic(TransactionCase):
""" % other_lang
})
rendered = view2.with_context(lang=current_lang)._render().strip()
rendered = self.env['ir.qweb'].with_context(lang=current_lang)._render(view2.id).strip()
self.assertEqual(rendered, '9/000/000*00')
def test_render_barcode(self):
@@ -1481,16 +1579,16 @@ class TestQWebBasic(TransactionCase):
})
view.arch = """<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'width': 100, 'height': 30}"/>"""
rendered = view._render(values={'partner': partner}).strip()
rendered = self.env['ir.qweb']._render(view.id, values={'partner': partner}).strip()
self.assertRegex(rendered, r'<div><img alt="Barcode test" src="data:image/png;base64,\S+"></div>')
partner.barcode = '4012345678901'
view.arch = """<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'symbology': 'EAN13', 'width': 100, 'height': 30, 'img_style': 'width:100%;', 'img_alt': 'Barcode'}"/>"""
ean_rendered = view._render(values={'partner': partner}).strip()
ean_rendered = self.env['ir.qweb']._render(view.id, values={'partner': partner}).strip()
self.assertRegex(ean_rendered, r'<div><img style="width:100%;" alt="Barcode" src="data:image/png;base64,\S+"></div>')
view.arch = """<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'symbology': 'auto', 'width': 100, 'height': 30, 'img_style': 'width:100%;', 'img_alt': 'Barcode'}"/>"""
auto_rendered = view._render(values={'partner': partner}).strip()
auto_rendered = self.env['ir.qweb']._render(view.id, values={'partner': partner}).strip()
self.assertRegex(auto_rendered, r'<div><img style="width:100%;" alt="Barcode" src="data:image/png;base64,\S+"></div>')
def test_render_comment_tail(self):
@@ -1513,7 +1611,7 @@ class TestQWebBasic(TransactionCase):
})
emptyline = '\n '
expected = markupsafe.Markup('Text 1' + emptyline + emptyline + 'Text 2' + emptyline + 'ok')
self.assertEqual(view1._render().strip(), expected)
self.assertEqual(self.env['ir.qweb']._render(view1.id).strip(), expected)
def test_void_element(self):
view = self.env['ir.ui.view'].create({
@@ -1676,7 +1774,7 @@ class FileSystemLoader(object):
if name:
yield name
def __call__(self, name, options):
def __call__(self, name):
for node in self.doc:
if node.get('t-name') == name:
return (deepcopy(node), name)
@@ -1719,7 +1817,7 @@ class TestQWebStaticXml(TransactionCase):
</t>
""")
}
def load(template_name, options):
def load(template_name):
return (templates[template_name], template_name)
rendering = render('html', {'val': 3}, load).strip()
@@ -825,7 +825,7 @@ class TestAssetsManifest(AddonManifestPatched):
def test_01_globmanifest(self):
view = self.make_asset_view('test_assetsbundle.manifest1')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest1.min.js')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -847,7 +847,7 @@ class TestAssetsManifest(AddonManifestPatched):
def test_02_globmanifest_no_duplicates(self):
view = self.make_asset_view('test_assetsbundle.manifest2')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest2.min.js')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -869,7 +869,7 @@ class TestAssetsManifest(AddonManifestPatched):
def test_03_globmanifest_file_before(self):
view = self.make_asset_view('test_assetsbundle.manifest3')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest3.min.js')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -896,7 +896,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.manifest4',
'path': 'test_assetsbundle/static/src/js/test_jsfile1.js',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4.min.js')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -917,7 +917,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.irasset1',
'path': 'test_assetsbundle/static/src/js/test_jsfile1.js',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.irasset1.min.js')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -937,7 +937,7 @@ class TestAssetsManifest(AddonManifestPatched):
'target': 'test_assetsbundle/static/src/js/test_jsfile1.js',
'path': 'http://external.link/external.js',
})
rendered = view._render()
rendered = self.env['ir.qweb']._render(view.id)
html_tree = lxml.etree.fromstring(rendered)
scripts = html_tree.findall('script')
self.assertEqual(len(scripts), 2)
@@ -968,7 +968,7 @@ class TestAssetsManifest(AddonManifestPatched):
'path': 'test_assetsbundle/static/src/js/test_jsfile1.js',
'target': 'test_assetsbundle/static/src/js/test_jsfile3.js',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -997,7 +997,7 @@ class TestAssetsManifest(AddonManifestPatched):
})
# asset is now: js_file1 ; js_file2 ; js_file3
# because js_file is replaced by 1 and 2
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1022,7 +1022,7 @@ class TestAssetsManifest(AddonManifestPatched):
'directive': 'remove',
'path': 'test_assetsbundle/static/src/js/test_jsfile2.js',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest5')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1054,7 +1054,7 @@ class TestAssetsManifest(AddonManifestPatched):
'path': 'test_assetsbundle/static/src/js/test_doesntexist.js',
})
with self.assertRaises(Exception) as cm:
view._render()
self.env['ir.qweb']._render(view.id)
self.assertTrue(
"['test_assetsbundle/static/src/js/test_doesntexist.js'] not found" in str(cm.exception)
)
@@ -1067,7 +1067,7 @@ class TestAssetsManifest(AddonManifestPatched):
'directive': 'remove',
'path': 'test_assetsbundle/static/src/**/*',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest2.js')], order='create_date DESC', limit=1)
# indeed everything in the bundle matches the glob, so there is no attachment
self.assertFalse(attach)
@@ -1080,7 +1080,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.manifest4',
'path': 'test_assetsbundle/static/src/js/test_jsfile1.js',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1102,7 +1102,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.irasset_include1',
'path': 'test_assetsbundle.manifest6',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.irasset_include1')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1115,7 +1115,7 @@ class TestAssetsManifest(AddonManifestPatched):
def test_12_include2(self):
view = self.make_asset_view('test_assetsbundle.manifest6')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest6')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1142,7 +1142,7 @@ class TestAssetsManifest(AddonManifestPatched):
})
with self.assertRaises(QWebException) as cm:
view._render()
self.env['ir.qweb']._render(view.id)
error = str(cm.exception.__cause__)
self.assertTrue(error)
self.assertFalse(isinstance(error, RecursionError))
@@ -1181,7 +1181,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.irasset_include3',
'path': 'test_assetsbundle/static/src/js/test_jsfile1.js',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.irasset_include1')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1205,7 +1205,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_other.mockmanifest1')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_other.mockmanifest1')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1229,7 +1229,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.manifest4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1256,7 +1256,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.manifest4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1283,7 +1283,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.manifest4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1308,7 +1308,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.manifest4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1332,7 +1332,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.manifest4')
rendered = view._render()
rendered = self.env['ir.qweb']._render(view.id)
html_tree = lxml.etree.fromstring(rendered)
scripts = html_tree.findall('script')
self.assertEqual(len(scripts), 2)
@@ -1373,7 +1373,7 @@ class TestAssetsManifest(AddonManifestPatched):
'media': 'print',
})
rendered = view._render()
rendered = self.env['ir.qweb']._render(view.id)
html_tree = lxml.etree.fromstring(rendered)
stylesheets = html_tree.findall('link')
self.assertEqual(len(stylesheets), 2)
@@ -1399,7 +1399,7 @@ class TestAssetsManifest(AddonManifestPatched):
't-css': 'true',
})
rendered = view._render()
rendered = self.env['ir.qweb']._render(view.id)
html_tree = lxml.etree.fromstring(rendered)
stylesheets = html_tree.findall('link')
self.assertEqual(len(stylesheets), 2)
@@ -1431,7 +1431,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.bundle4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1461,7 +1461,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.bundle4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1495,7 +1495,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.bundle4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1525,7 +1525,7 @@ class TestAssetsManifest(AddonManifestPatched):
}
}
view = self.make_asset_view('test_assetsbundle.bundle4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1554,7 +1554,7 @@ class TestAssetsManifest(AddonManifestPatched):
'directive': 'before',
})
view = self.make_asset_view('test_assetsbundle.bundle4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1583,7 +1583,7 @@ class TestAssetsManifest(AddonManifestPatched):
'directive': 'after',
})
view = self.make_asset_view('test_assetsbundle.bundle4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1622,7 +1622,7 @@ class TestAssetsManifest(AddonManifestPatched):
't-js': 'true',
't-css': 'true',
})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.bundle4')], order='create_date DESC', limit=2)
attach_css = None
attach_js = None
@@ -1680,7 +1680,7 @@ class TestAssetsManifest(AddonManifestPatched):
})
view = self.make_asset_view('test_assetsbundle.wrong_path')
with self.assertRaises(Exception) as cm:
view._render()
self.env['ir.qweb']._render(view.id)
self.assertTrue(
"test_assetsbundle/static/src/js/doesnt_exist.js not found" in str(cm.exception)
)
@@ -1694,7 +1694,7 @@ class TestAssetsManifest(AddonManifestPatched):
'directive': 'after',
})
view = self.make_asset_view('test_assetsbundle.manifest4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1723,7 +1723,7 @@ class TestAssetsManifest(AddonManifestPatched):
'directive': 'before',
})
view = self.make_asset_view('test_assetsbundle.manifest4')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.manifest4')], order='create_date DESC', limit=1)
content = attach.raw.decode()
self.assertStringEqual(
@@ -1756,7 +1756,7 @@ class TestAssetsManifest(AddonManifestPatched):
'path': '/test_assetsbundle/%s' % path_to_dummy,
})
view = self.make_asset_view('test_assetsbundle.irassetsec')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.irassetsec')], order='create_date DESC', limit=1)
self.assertFalse(attach.exists())
@@ -1789,7 +1789,7 @@ class TestAssetsManifest(AddonManifestPatched):
})
view = self.make_asset_view('test_assetsbundle.irassetsec')
with self.assertRaises(QWebException) as cm:
view._render()
self.env['ir.qweb']._render(view.id)
self.assertTrue('Unallowed to fetch files from addon notinstalled_module' in str(cm.exception))
@@ -1811,7 +1811,7 @@ class TestAssetsManifest(AddonManifestPatched):
'path': '/test_assetsbundle/__manifest__.py',
})
view = self.make_asset_view('test_assetsbundle.irassetsec')
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.irassetsec')], order='create_date DESC', limit=1)
self.assertFalse(attach.exists())
@@ -1857,7 +1857,7 @@ class TestAssetsManifest(AddonManifestPatched):
'path': 'test_assetsbundle/my_style_attach.scss',
})
view = self.make_asset_view('test_assetsbundle.irasset_custom_attach', {'t-css': True})
view._render()
self.env['ir.qweb']._render(view.id)
attach = self.env['ir.attachment'].search([('name', 'ilike', 'test_assetsbundle.irasset_custom_attach')], order='create_date DESC', limit=1)
content = attach.raw.decode()
# The scss should be compiled
+8 -6
View File
@@ -292,14 +292,14 @@ class QwebTracker():
@classmethod
def wrap_compile(cls, method_compile):
@functools.wraps(method_compile)
def _tracked_compile(self, template, options):
if not options.get('profile'):
return method_compile(self, template, options)
def _tracked_compile(self, template):
if not self.env.context.get('profile'):
return method_compile(self, template)
render_template = method_compile(self, template, options)
render_template = method_compile(self, template)
def profiled_method_compile(self, values):
ref = options.get('ref')
ref_xml = options.get('ref_xml')
ref = render_template.options.get('ref')
ref_xml = render_template.options.get('ref_xml')
qweb_tracker = QwebTracker(ref, ref_xml, self.env.cr)
self = self.with_context(qweb_tracker=qweb_tracker)
if qweb_tracker.execution_context_enabled:
@@ -348,6 +348,8 @@ class QwebTracker():
directive_info[key] = repr(attrib[key])
elif directive == 'foreach':
directive_info['t-as'] = repr(attrib['t-as'])
elif directive == 'groups' and 'groups' in attrib and not directive_info.get('t-groups'):
directive_info['t-groups'] = repr(attrib['groups'])
elif directive == 'att':
for key in attrib:
if key.startswith('t-att-') or key.startswith('t-attf-'):