From 880954ebfc1106411b7f7a7d60aee05dfae60893 Mon Sep 17 00:00:00 2001 From: Gorash Date: Thu, 1 Apr 2021 15:26:18 +0000 Subject: [PATCH] [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 --- addons/account/controllers/onboarding.py | 4 +- addons/account/models/company.py | 10 +- .../wizard/account_tour_upload_bill.py | 2 +- .../models/account_edi_format.py | 11 +- .../models/account_edi_format.py | 2 +- addons/crm/tests/test_performances.py | 4 +- addons/hr_expense/models/hr_expense.py | 3 +- .../tests/test_performance.py | 2 +- addons/http_routing/models/__init__.py | 2 +- addons/http_routing/models/ir_qweb.py | 52 +++ addons/http_routing/models/ir_ui_view.py | 16 - .../im_livechat/models/im_livechat_channel.py | 3 +- addons/im_livechat/models/mail_channel.py | 3 +- .../l10n_be_edi/models/account_edi_format.py | 2 +- addons/l10n_it_edi/models/account_invoice.py | 2 +- .../l10n_nl_edi/models/account_edi_format.py | 2 +- .../l10n_no_edi/models/account_edi_format.py | 2 +- addons/mail/models/mail_activity.py | 16 +- addons/mail/models/mail_activity_mixin.py | 9 +- addons/mail/models/mail_alias.py | 5 +- addons/mail/models/mail_render_mixin.py | 53 ++- addons/mail/models/mail_template.py | 56 ++- addons/mail/models/mail_thread.py | 31 +- addons/mail_group/controllers/portal.py | 2 +- addons/mail_group/models/mail_group.py | 7 +- addons/mail_group/tests/test_mail_group.py | 3 +- .../wizard/mail_compose_message.py | 9 +- .../wizard/mailing_mailing_test.py | 4 +- addons/mrp/models/mrp_production.py | 4 +- addons/mrp/report/mrp_report_bom_structure.py | 8 +- addons/payment/models/payment_transaction.py | 2 +- .../payment_paypal/models/payment_acquirer.py | 9 +- addons/portal/models/__init__.py | 1 + addons/portal/models/ir_qweb.py | 26 ++ addons/portal/models/ir_ui_view.py | 22 +- .../models/pos_mercury_transaction.py | 2 +- .../report/product_pricelist_report.py | 2 +- addons/project/models/project.py | 3 +- addons/project/models/project_update.py | 3 +- addons/purchase_stock/models/purchase.py | 2 +- addons/sale/controllers/onboarding.py | 2 +- addons/sale_stock/models/sale_order.py | 2 +- addons/sale_stock/models/stock.py | 2 +- addons/snailmail/models/__init__.py | 1 - addons/snailmail/models/ir_qweb_fields.py | 19 - addons/stock/models/stock_picking.py | 2 +- addons/stock/report/stock_traceability.py | 2 +- addons/survey/controllers/main.py | 12 +- .../controllers/survey_session_manage.py | 4 +- addons/survey/wizard/survey_invite.py | 20 +- .../test_event_full/tests/test_performance.py | 28 +- addons/test_mail/tests/test_performance.py | 4 +- addons/web/controllers/main.py | 2 +- addons/web/models/base_document_layout.py | 8 +- addons/web/models/ir_qweb_fields.py | 2 +- addons/web/models/models.py | 35 +- addons/web_editor/models/ir_qweb_fields.py | 1 - addons/web_editor/models/ir_ui_view.py | 11 - addons/website/controllers/main.py | 2 - addons/website/models/ir_http.py | 2 +- addons/website/models/ir_qweb.py | 76 +++- addons/website/models/ir_ui_view.py | 74 +--- addons/website/models/website.py | 16 +- .../website/models/website_snippet_filter.py | 3 +- addons/website/tests/test_performance.py | 2 +- addons/website/tests/test_qweb.py | 2 +- addons/website/tests/test_views.py | 4 +- addons/website_blog/tests/test_performance.py | 4 +- .../views/event_templates_list.xml | 12 +- .../event_templates_page_registration.xml | 14 +- .../views/event_exhibitor_templates_page.xml | 4 +- .../views/event_meet_templates_list.xml | 2 +- .../models/hr_recruitment.py | 5 +- .../models/payment_transaction.py | 4 +- .../models/product_wishlist.py | 3 +- addons/website_slides/controllers/main.py | 2 +- .../wizard/slide_channel_invite.py | 26 +- odoo/addons/base/models/ir_actions_report.py | 32 +- odoo/addons/base/models/ir_qweb.py | 331 +++++++++++------- odoo/addons/base/models/ir_qweb_fields.py | 19 +- odoo/addons/base/models/ir_ui_view.py | 72 +--- odoo/addons/base/tests/test_profiler.py | 2 + odoo/addons/base/tests/test_qweb.py | 184 +++++++--- .../tests/test_assetsbundle.py | 76 ++-- odoo/tools/profiler.py | 14 +- 85 files changed, 797 insertions(+), 748 deletions(-) create mode 100644 addons/http_routing/models/ir_qweb.py delete mode 100644 addons/http_routing/models/ir_ui_view.py create mode 100644 addons/portal/models/ir_qweb.py delete mode 100644 addons/snailmail/models/ir_qweb_fields.py diff --git a/addons/account/controllers/onboarding.py b/addons/account/controllers/onboarding.py index 3c0afa38c3a..688cda338a0 100644 --- a/addons/account/controllers/onboarding.py +++ b/addons/account/controllers/onboarding.py @@ -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() }) diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 4100f2ba29c..1e1fbbbac8b 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -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 diff --git a/addons/account/wizard/account_tour_upload_bill.py b/addons/account/wizard/account_tour_upload_bill.py index 8ff164c7ba6..9d49c168805 100644 --- a/addons/account/wizard/account_tour_upload_bill.py +++ b/addons/account/wizard/account_tour_upload_bill.py @@ -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, diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py index cbe8bd38a65..9c2992d5476 100644 --- a/addons/account_edi_facturx/models/account_edi_format.py +++ b/addons/account_edi_facturx/models/account_edi_format.py @@ -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_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(), diff --git a/addons/account_edi_ubl/models/account_edi_format.py b/addons/account_edi_ubl/models/account_edi_format.py index 9a605c402f4..a20f7e38af3 100644 --- a/addons/account_edi_ubl/models/account_edi_format.py +++ b/addons/account_edi_ubl/models/account_edi_format.py @@ -207,7 +207,7 @@ class AccountEdiFormat(models.Model): self.ensure_one() # Create file content. xml_content = markupsafe.Markup("") - 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, diff --git a/addons/crm/tests/test_performances.py b/addons/crm/tests/test_performances.py index aa698c1d456..a0342faa2ee 100644 --- a/addons/crm/tests/test_performances.py +++ b/addons/crm/tests/test_performances.py @@ -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 diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 7ca1d669352..e23733acf43 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -846,8 +846,7 @@ Or send your receipts at ") - 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, diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py index 1f2d3b60966..34df843ada4 100644 --- a/addons/l10n_it_edi/models/account_invoice.py +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -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): diff --git a/addons/l10n_nl_edi/models/account_edi_format.py b/addons/l10n_nl_edi/models/account_edi_format.py index 503b9afb78d..576ea8bd24d 100644 --- a/addons/l10n_nl_edi/models/account_edi_format.py +++ b/addons/l10n_nl_edi/models/account_edi_format.py @@ -67,7 +67,7 @@ class AccountEdiFormat(models.Model): self.ensure_one() # Create file content. xml_content = markupsafe.Markup("") - 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({ diff --git a/addons/l10n_no_edi/models/account_edi_format.py b/addons/l10n_no_edi/models/account_edi_format.py index 62fa00a0629..226148bfdd0 100644 --- a/addons/l10n_no_edi/models/account_edi_format.py +++ b/addons/l10n_no_edi/models/account_edi_format.py @@ -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({ diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index b8887ec2c73..a7a5acff8ef 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -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 diff --git a/addons/mail/models/mail_activity_mixin.py b/addons/mail/models/mail_activity_mixin.py index 7d656bc96b6..11e7fa79a6b 100644 --- a/addons/mail/models/mail_activity_mixin.py +++ b/addons/mail/models/mail_activity_mixin.py @@ -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 diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index d5c04b766f3..d9aeebb0b03 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -263,8 +263,7 @@ Kind Regards,

""" 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) diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index 0f78ca97639..a43df861a14 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -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 diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 43dd8377ab6..cca08844261 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -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 diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index ef2fe1c7631..aea4cd8e184 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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), diff --git a/addons/mail_group/controllers/portal.py b/addons/mail_group/controllers/portal.py index 2f048b58df6..3fa335a8995 100644 --- a/addons/mail_group/controllers/portal.py +++ b/addons/mail_group/controllers/portal.py @@ -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 diff --git a/addons/mail_group/models/mail_group.py b/addons/mail_group/models/mail_group.py index 1ea25039741..125095861e2 100644 --- a/addons/mail_group/models/mail_group.py +++ b/addons/mail_group/models/mail_group.py @@ -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, diff --git a/addons/mail_group/tests/test_mail_group.py b/addons/mail_group/tests/test_mail_group.py index a604a240177..30c032edebb 100644 --- a/addons/mail_group/tests/test_mail_group.py +++ b/addons/mail_group/tests/test_mail_group.py @@ -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("
Test email body
", footer, plaintext=False) result = self.env['mail.group']._clean_email_body(body) diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index 4072166f4bc..db7f71cfd36 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -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, diff --git a/addons/mass_mailing/wizard/mailing_mailing_test.py b/addons/mass_mailing/wizard/mailing_mailing_test.py index fe1d22fa68a..0006295a9ca 100644 --- a/addons/mass_mailing/wizard/mailing_mailing_test.py +++ b/addons/mass_mailing/wizard/mailing_mailing_test.py @@ -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], diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index a9e86241325..d1c90f9a68e 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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) diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 91d66ae2dde..788aabee1ed 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -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): diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index c1aa6cdb622..e213ba4a5ee 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -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 diff --git a/addons/payment_paypal/models/payment_acquirer.py b/addons/payment_paypal/models/payment_acquirer.py index b925baaa368..f9af41b6f29 100644 --- a/addons/payment_paypal/models/payment_acquirer.py +++ b/addons/payment_paypal/models/payment_acquirer.py @@ -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, diff --git a/addons/portal/models/__init__.py b/addons/portal/models/__init__.py index e4169356b07..f49f180354e 100644 --- a/addons/portal/models/__init__.py +++ b/addons/portal/models/__init__.py @@ -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 diff --git a/addons/portal/models/ir_qweb.py b/addons/portal/models/ir_qweb.py new file mode 100644 index 00000000000..9d85910a7d4 --- /dev/null +++ b/addons/portal/models/ir_qweb.py @@ -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 diff --git a/addons/portal/models/ir_ui_view.py b/addons/portal/models/ir_ui_view.py index 64a63bfee0d..f84d487e938 100644 --- a/addons/portal/models/ir_ui_view.py +++ b/addons/portal/models/ir_ui_view.py @@ -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 diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py index 96c00beea73..7ce0811d714 100644 --- a/addons/pos_mercury/models/pos_mercury_transaction.py +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -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" diff --git a/addons/product/report/product_pricelist_report.py b/addons/product/report/product_pricelist_report.py index f4dee8f0ab0..6b3bed804d8 100644 --- a/addons/product/report/product_pricelist_report.py +++ b/addons/product/report/product_pricelist_report.py @@ -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] diff --git a/addons/project/models/project.py b/addons/project/models/project.py index ee4de860498..f1b5e2c9645 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -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), diff --git a/addons/project/models/project_update.py b/addons/project/models/project_update.py index 85ee35f5185..789feb050c9 100644 --- a/addons/project/models/project_update.py +++ b/addons/project/models/project_update.py @@ -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): diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py index 3eef4341695..3f57e2aa680 100644 --- a/addons/purchase_stock/models/purchase.py +++ b/addons/purchase_stock/models/purchase.py @@ -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 = {} diff --git a/addons/sale/controllers/onboarding.py b/addons/sale/controllers/onboarding.py index 8b54c0d3b23..40f3c2c5fa6 100644 --- a/addons/sale/controllers/onboarding.py +++ b/addons/sale/controllers/onboarding.py @@ -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() }) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 3ef0cfd6605..38f8e532b00 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -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) diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 43c5ddebc7b..25f19e5a376 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -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) diff --git a/addons/snailmail/models/__init__.py b/addons/snailmail/models/__init__.py index 0126d49e07d..3c7ec4a947b 100644 --- a/addons/snailmail/models/__init__.py +++ b/addons/snailmail/models/__init__.py @@ -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 diff --git a/addons/snailmail/models/ir_qweb_fields.py b/addons/snailmail/models/ir_qweb_fields.py deleted file mode 100644 index b005fdd342f..00000000000 --- a/addons/snailmail/models/ir_qweb_fields.py +++ /dev/null @@ -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) diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index c0a8d6c59a9..83e20a07cb3 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -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) diff --git a/addons/stock/report/stock_traceability.py b/addons/stock/report/stock_traceability.py index 65c1082d0a3..8699148f400 100644 --- a/addons/stock/report/stock_traceability.py +++ b/addons/stock/report/stock_traceability.py @@ -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 diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index fa16c877148..fafe7f03ee5 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -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 } diff --git a/addons/survey/controllers/survey_session_manage.py b/addons/survey/controllers/survey_session_manage.py index 80811927127..a08c76105ad 100644 --- a/addons/survey/controllers/survey_session_manage.py +++ b/addons/survey/controllers/survey_session_manage.py @@ -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() }) diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py index d2b34cf06dc..bf91351d326 100644 --- a/addons/survey/wizard/survey_invite.py +++ b/addons/survey/wizard/survey_invite.py @@ -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) diff --git a/addons/test_event_full/tests/test_performance.py b/addons/test_event_full/tests/test_performance.py index cdba4916122..217e4519145 100644 --- a/addons/test_event_full/tests/test_performance.py +++ b/addons/test_event_full/tests/test_performance.py @@ -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], diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 43721eef50e..ca7ffe318ed 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -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']) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 34497de6b28..2f4bfefe7ef 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -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) diff --git a/addons/web/models/base_document_layout.py b/addons/web/models/base_document_layout.py index b73be78821e..36d8a511dfc 100644 --- a/addons/web/models/base_document_layout.py +++ b/addons/web/models/base_document_layout.py @@ -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 diff --git a/addons/web/models/ir_qweb_fields.py b/addons/web/models/ir_qweb_fields.py index d68ff6ff971..57f39c7a404 100644 --- a/addons/web/models/ir_qweb_fields.py +++ b/addons/web/models/ir_qweb_fields.py @@ -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 = [' + new_homepage_view = '''
-''' % (self.id) +''' 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()) diff --git a/addons/website/models/website_snippet_filter.py b/addons/website/models/website_snippet_filter.py index a9ba2f81f5c..3a168257e9c 100644 --- a/addons/website/models/website_snippet_filter.py +++ b/addons/website/models/website_snippet_filter.py @@ -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, )) diff --git a/addons/website/tests/test_performance.py b/addons/website/tests/test_performance.py index 8cf0dcbeadd..f62e0de5d67 100644 --- a/addons/website/tests/test_performance.py +++ b/addons/website/tests/test_performance.py @@ -116,7 +116,7 @@ class TestWebsitePerformance(UtilPerf): # website.page with no call to layout templates self.page.arch = '
I am a blank page
' 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 diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py index 6561f3f05ea..c76e07e6cdd 100644 --- a/addons/website/tests/test_qweb.py +++ b/addons/website/tests/test_qweb.py @@ -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 ) diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index edcc55859ca..49ffb93c698 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -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' ) diff --git a/addons/website_blog/tests/test_performance.py b/addons/website_blog/tests/test_performance.py index 979ab7b32f9..eb7acccabcf 100644 --- a/addons/website_blog/tests/test_performance.py +++ b/addons/website_blog/tests/test_performance.py @@ -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'] diff --git a/addons/website_event/views/event_templates_list.xml b/addons/website_event/views/event_templates_list.xml index e5dc652052d..9dae437d4d6 100644 --- a/addons/website_event/views/event_templates_list.xml +++ b/addons/website_event/views/event_templates_list.xml @@ -173,8 +173,8 @@
- - + +
@@ -198,10 +198,10 @@ diff --git a/addons/website_event/views/event_templates_page_registration.xml b/addons/website_event/views/event_templates_page_registration.xml index 26bea969048..6f0a2d81ccb 100644 --- a/addons/website_event/views/event_templates_page_registration.xml +++ b/addons/website_event/views/event_templates_page_registration.xml @@ -42,25 +42,25 @@
Date & Time
-
-
+
+
Start - - + - + () ()
-
-
+
+
End - - + ()
diff --git a/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml b/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml index 9365bd9de0b..ee237d93b16 100644 --- a/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml +++ b/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml @@ -50,8 +50,8 @@
starts on - (). + ().
Attendees will be able to join to meet . diff --git a/addons/website_event_meet/views/event_meet_templates_list.xml b/addons/website_event_meet/views/event_meet_templates_list.xml index 39e4750a18e..18908fe9130 100644 --- a/addons/website_event_meet/views/event_meet_templates_list.xml +++ b/addons/website_event_meet/views/event_meet_templates_list.xml @@ -74,7 +74,7 @@ This room is not open right now!
Join us here on the - + () to have a chat with us!
diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py index cd44a05586f..3eec7c3d0f2 100644 --- a/addons/website_hr_recruitment/models/hr_recruitment.py +++ b/addons/website_hr_recruitment/models/hr_recruitment.py @@ -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) diff --git a/addons/website_payment/models/payment_transaction.py b/addons/website_payment/models/payment_transaction.py index cfc8a628cbe..d8a50876b14 100644 --- a/addons/website_payment/models/payment_transaction.py +++ b/addons/website_payment/models/payment_transaction.py @@ -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", diff --git a/addons/website_sale_stock_wishlist/models/product_wishlist.py b/addons/website_sale_stock_wishlist/models/product_wishlist.py index 3630aa1c81f..9b516db6e8f 100644 --- a/addons/website_sale_stock_wishlist/models/product_wishlist.py +++ b/addons/website_sale_stock_wishlist/models/product_wishlist.py @@ -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", diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index efe110aa81c..89eaa62da78 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -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, }) diff --git a/addons/website_slides/wizard/slide_channel_invite.py b/addons/website_slides/wizard/slide_channel_invite.py index 2aee778a3ce..7360f621628 100644 --- a/addons/website_slides/wizard/slide_channel_invite.py +++ b/addons/website_slides/wizard/slide_channel_invite.py @@ -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 diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 54b8ce11103..817aed48c19 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -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() diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index bb9d574224d..9e222ed96d9 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -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) diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py index 3080c3b6fc2..5bf43e581ad 100644 --- a/odoo/addons/base/models/ir_qweb_fields.py +++ b/odoo/addons/base/models/ir_qweb_fields.py @@ -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', {})) diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 7e15350b7fa..f29f95c20c1 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -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 diff --git a/odoo/addons/base/tests/test_profiler.py b/odoo/addons/base/tests/test_profiler.py index e75bcad0fd3..a47259a839d 100644 --- a/odoo/addons/base/tests/test_profiler.py +++ b/odoo/addons/base/tests/test_profiler.py @@ -461,6 +461,7 @@ class TestProfiling(TransactionCase): template = self.env['ir.ui.view'].create({ 'name': 'test', 'type': 'qweb', + 'key': 'root', 'arch_db': ''' [: ] @@ -470,6 +471,7 @@ class TestProfiling(TransactionCase): child_template = self.env['ir.ui.view'].create({ 'name': 'test', 'type': 'qweb', + 'key': 'dummy', 'arch_db': ' ' }) self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)" diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py index e731cc93ebd..885a3d9bf33 100644 --- a/odoo/addons/base/tests/test_qweb.py +++ b/odoo/addons/base/tests/test_qweb.py @@ -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): """ }) - 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): """ }) - rendered = view._render({'malicious': '1