[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:
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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'&'),
|
||||
str(self.env['ir.qweb']._render(view.id)).replace(u'&', u'&'),
|
||||
'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 & 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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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', {}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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 <b>c</b></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
|
||||
|
||||
@@ -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-'):
|
||||
|
||||
Reference in New Issue
Block a user