[IMP] all/website: using lazy values and t-cache in templates

Using lazy values allows you to not do query when the content is not
displayed or if it is already cached. Adding `t-cache` only reduces
render time, but combining it with lazy values saves browses, computes
and query.
Thus for the `/shop` page the page is displayed in 60ms (before: 250ms).

Part-of: odoo/odoo#88276
This commit is contained in:
Gorash
2022-06-03 16:40:40 +02:00
parent 9af92b7374
commit b0a2a41d78
32 changed files with 330 additions and 192 deletions
+3 -3
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools import is_html_empty
from odoo.tools import is_html_empty, lazy
class IrQWeb(models.AbstractModel):
@@ -15,9 +15,9 @@ class IrQWeb(models.AbstractModel):
irQweb = super()._prepare_frontend_environment(values)
values.update(
is_html_empty=is_html_empty,
languages=[lang for
languages=lazy(lambda: [lang for
lang in irQweb.env['res.lang'].get_available()
if lang[0] in irQweb.env['ir.http']._get_frontend_langs()]
if lang[0] in irQweb.env['ir.http']._get_frontend_langs()])
)
for key in irQweb.env.context:
if key not in values:
+72 -52
View File
@@ -6,25 +6,27 @@
<attribute name="t-attf-class" add="#{'o_portal' if is_portal else ''}" separator=" "/>
</xpath>
<xpath expr="//div[@id='wrapwrap']/header/img" position="replace">
<nav class="navbar navbar-expand navbar-light bg-light">
<div class="container">
<a href="/" class="navbar-brand logo">
<img t-att-src="'/logo.png?company=%s' % res_company.id" t-att-alt="'Logo of %s' % res_company.name" t-att-title="res_company.name"/>
</a>
<ul id="top_menu" class="nav navbar-nav ml-auto">
<t t-call="portal.placeholder_user_sign_in">
<t t-set="_item_class" t-value="'nav-item'"/>
<t t-set="_link_class" t-value="'nav-link'"/>
</t>
<t t-call="portal.user_dropdown">
<t t-set="_user_name" t-value="true"/>
<t t-set="_item_class" t-value="'nav-item dropdown'"/>
<t t-set="_link_class" t-value="'nav-link'"/>
<t t-set="_dropdown_menu_class" t-value="'dropdown-menu-right'"/>
</t>
</ul>
</div>
</nav>
<t t-cache="res_company">
<nav class="navbar navbar-expand navbar-light bg-light">
<div class="container">
<a href="/" class="navbar-brand logo">
<img t-att-src="'/logo.png?company=%s' % res_company.id" t-att-alt="'Logo of %s' % res_company.name" t-att-title="res_company.name"/>
</a>
<ul id="top_menu" class="nav navbar-nav ml-auto">
<t t-call="portal.placeholder_user_sign_in">
<t t-set="_item_class" t-value="'nav-item'"/>
<t t-set="_link_class" t-value="'nav-link'"/>
</t>
<t t-call="portal.user_dropdown">
<t t-set="_user_name" t-value="true"/>
<t t-set="_item_class" t-value="'nav-item dropdown'"/>
<t t-set="_link_class" t-value="'nav-link'"/>
<t t-set="_dropdown_menu_class" t-value="'dropdown-menu-right'"/>
</t>
</ul>
</div>
</nav>
</t>
</xpath>
<xpath expr="//div[@id='wrapwrap']/main/t[@t-out='0']" position="before">
<div t-if="o_portal_fullwidth_alert" class="alert alert-info alert-dismissible rounded-0 fade show d-print-none css_editable_mode_hidden">
@@ -45,42 +47,57 @@
</template>
<template id="language_selector" name="Language Selector">
<t t-set="active_lang" t-value="list(filter(lambda lg : lg[0] == lang, languages))[0]"/>
<t t-set="language_selector_visible" t-value="len(languages) &gt; 1"/>
<div t-attf-class="js_language_selector #{_div_classes} d-print-none" t-if="language_selector_visible">
<button t-attf-class="btn btn-sm btn-outline-secondary border-0 dropdown-toggle #{_btn_class}" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span t-if="not no_text"
class="align-middle"
t-esc="active_lang[2].split('/').pop()"/>
</button>
<div t-attf-class="dropdown-menu #{_dropdown_menu_class}" role="menu">
<t t-foreach="languages" t-as="lg">
<a t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang_code=lg[0])"
t-attf-class="dropdown-item js_change_lang #{active_lang == lg and 'active'}"
t-att-data-url_code="lg[1]">
<span t-if="not no_text" t-esc="lg[2].split('/').pop()"/>
</a>
</t>
<t t-nocache="The query strings can change for the same page and the same rendering."
t-nocache-no_text="no_text"
t-nocache-_div_classes="_div_classes"
t-nocache-_btn_class="_btn_class"
t-nocache-_dropdown_menu_class="_dropdown_menu_class">
<t t-set="active_lang" t-value="list(filter(lambda lg : lg[0] == lang, languages))[0]"/>
<t t-set="language_selector_visible" t-value="len(languages) &gt; 1"/>
<div t-attf-class="js_language_selector #{_div_classes} d-print-none" t-if="language_selector_visible">
<button t-attf-class="btn btn-sm btn-outline-secondary border-0 dropdown-toggle #{_btn_class}" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span t-if="not no_text"
class="align-middle"
t-esc="active_lang[2].split('/').pop()"/>
</button>
<div t-attf-class="dropdown-menu #{_dropdown_menu_class}" role="menu">
<t t-foreach="languages" t-as="lg">
<a t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang_code=lg[0])"
t-attf-class="dropdown-item js_change_lang #{active_lang == lg and 'active'}"
t-att-data-url_code="lg[1]">
<span t-if="not no_text" t-esc="lg[2].split('/').pop()"/>
</a>
</t>
</div>
</div>
</div>
</t>
</template>
<template id="user_dropdown" name="Portal User Dropdown">
<t t-set="is_connected" t-value="not user_id._is_public()"/>
<li t-if="is_connected" t-attf-class="#{_item_class} o_no_autohide_item">
<a href="#" role="button" data-toggle="dropdown" t-attf-class="dropdown-toggle #{_link_class}">
<t t-if="_avatar">
<t t-set="avatar_source" t-value="image_data_uri(user_id.avatar_256)"/>
<img t-att-src="avatar_source" t-attf-class="rounded-circle o_object_fit_cover #{_avatar_class}" width="24" height="24" alt="" loading="eager"/>
</t>
<i t-if="_icon" t-attf-class="fa fa-1x fa-fw fa-user-circle-o #{_icon_class}"/>
<span t-if="_user_name" t-attf-class="#{_user_name_class}" t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) &gt; 25 else user_id.name"/>
</a>
<div t-attf-class="dropdown-menu js_usermenu #{_dropdown_menu_class}" role="menu">
<div id="o_logout_divider" class="dropdown-divider"/>
<a t-attf-href="/web/session/logout?redirect=/" role="menuitem" id="o_logout" class="dropdown-item">Logout</a>
</div>
</li>
<t t-nocache="Each user is different regardless of the page visited."
t-nocache-_icon="_icon"
t-nocache-_icon_class="_icon_class"
t-nocache-_user_name="_user_name"
t-nocache-_user_name_class="_user_name_class"
t-nocache-_item_class="_item_class"
t-nocache-_link_class="_link_class"
t-nocache-_dropdown_menu_class="_dropdown_menu_class">
<t t-set="is_connected" t-value="not user_id._is_public()"/>
<li t-if="is_connected" t-attf-class="#{_item_class} o_no_autohide_item">
<a href="#" role="button" data-toggle="dropdown" t-attf-class="dropdown-toggle #{_link_class}">
<t t-if="_avatar">
<t t-set="avatar_source" t-value="image_data_uri(user_id.avatar_256)"/>
<img t-att-src="avatar_source" t-attf-class="rounded-circle o_object_fit_cover #{_avatar_class}" width="24" height="24" alt="" loading="eager"/>
</t>
<i t-if="_icon" t-attf-class="fa fa-1x fa-fw fa-user-circle-o #{_icon_class}"/>
<span t-if="_user_name" t-attf-class="#{_user_name_class}" t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) &gt; 25 else user_id.name"/>
</a>
<div t-attf-class="dropdown-menu js_usermenu #{_dropdown_menu_class}" role="menu">
<div id="o_logout_divider" class="dropdown-divider"/>
<a t-attf-href="/web/session/logout?redirect=/" role="menuitem" id="o_logout" class="dropdown-item">Logout</a>
</div>
</li>
</t>
</template>
<template id="portal_breadcrumbs" name="Portal Breadcrumbs">
@@ -149,7 +166,10 @@
<template id="user_sign_in" name="User Sign In" inherit_id="portal.placeholder_user_sign_in">
<xpath expr="." position="inside">
<li groups="base.group_public" t-attf-class="#{_item_class} o_no_autohide_item">
<li t-nocache="Profile session and user group can change unrelated to parent caches."
t-nocache-_item_class="_item_class"
t-nocache-_link_class="_link_class"
groups="base.group_public" t-attf-class="#{_item_class} o_no_autohide_item">
<a t-attf-href="/web/login" t-attf-class="#{_link_class}">Sign in<span t-if="request.session.profile_session" class="text-danger fa fa-circle"/></a>
</li>
</xpath>
+2 -2
View File
@@ -37,7 +37,7 @@
<t t-call-assets="web.assets_frontend" t-js="false"/>
</xpath>
<xpath expr="//head/script[@id='web.layout.odooscript']" position="after">
<script type="text/javascript">
<script t-nocache="Session information should always be up to date." type="text/javascript">
odoo.__session_info__ = <t t-out="json.dumps(request.env['ir.http'].get_frontend_session_info())"/>;
if (!/(^|;\s)tz=/.test(document.cookie)) {
const userTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -60,7 +60,7 @@
<main>
<t t-out="0"/>
</main>
<footer t-if="not no_footer" id="bottom" data-anchor="true" t-attf-class="bg-light o_footer">
<footer t-cache="no_footer,no_copyright" t-if="not no_footer" id="bottom" data-anchor="true" t-attf-class="bg-light o_footer">
<div id="footer"/>
<div t-if="not no_copyright" class="o_footer_copyright">
<div class="container py-3">
+12 -10
View File
@@ -5,6 +5,7 @@ from collections import OrderedDict
from odoo import models
from odoo.http import request
from odoo.tools import lazy
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.osv import expression
@@ -60,31 +61,32 @@ class IrQWeb(models.AbstractModel):
Website = irQweb.env['website']
editable = request.website.is_publisher()
translatable = editable and irQweb.env.context.get('lang') != irQweb.env['ir.http']._get_default_lang().code
editable = not translatable and editable
editable = editable and not translatable
current_website = Website.get_current_website()
has_group_publisher = irQweb.env.user.has_group('website.group_website_publisher')
if has_group_publisher and irQweb.env.user.has_group('website.group_multi_website'):
values['multi_website_websites_current'] = current_website.name
values['multi_website_websites'] = [
values['multi_website_websites_current'] = lazy(lambda: current_website.name)
values['multi_website_websites'] = lazy(lambda: [
{'website_id': website.id, 'name': website.name, 'domain': website.domain}
for website in Website.search([('id', '!=', current_website.id)])
]
])
cur_company = irQweb.env.company
values['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name}
values['multi_website_companies'] = [
values['multi_website_companies_current'] = lazy(lambda: {'company_id': cur_company.id, 'name': cur_company.name})
values['multi_website_companies'] = lazy(lambda: [
{'company_id': comp.id, 'name': comp.name}
for comp in irQweb.env.user.company_ids if comp != cur_company
]
])
# update values
values.update(dict(
website=current_website,
is_view_active=current_website.is_view_active,
res_company=request.env['res.company'].browse(current_website._get_cached('company_id')).sudo(),
is_view_active=lazy(lambda: current_website.is_view_active),
res_company=lazy(request.env['res.company'].browse(current_website._get_cached('company_id')).sudo),
translatable=translatable,
editable=editable,
))
@@ -93,7 +95,7 @@ class IrQWeb(models.AbstractModel):
# form editable object, add the backend configuration link
if 'main_object' in values and has_group_publisher:
func = getattr(values['main_object'], 'get_backend_menu_id', False)
values['backend_menu_id'] = func and func() or irQweb.env['ir.model.data']._xmlid_to_res_id('website.menu_website_configuration')
values['backend_menu_id'] = lazy(lambda: func and func() or irQweb.env['ir.model.data']._xmlid_to_res_id('website.menu_website_configuration'))
# update options
+13 -6
View File
@@ -361,8 +361,9 @@ class View(models.Model):
Archived views are ignored (unless the active_test context is set, but
then the ormcache_context will not work as expected).
"""
if 'website_id' in self._context and not isinstance(xml_id, int):
current_website = self.env['website'].browse(self._context.get('website_id'))
website_id = self._context.get('website_id')
if website_id and not isinstance(xml_id, int):
current_website = self.env['website'].browse(int(website_id))
domain = ['&', ('key', '=', xml_id)] + current_website.website_domain()
view = self.sudo().search(domain, order='website_id', limit=1)
@@ -372,6 +373,10 @@ class View(models.Model):
return view.id
return super(View, self.sudo())._get_view_id(xml_id)
@tools.ormcache('self.id')
def _get_cached_visibility(self):
return self.visibility
def _handle_visibility(self, do_raise=True):
""" Check the visibility set on the main view and raise 403 if you should not have access.
Order is: Public, Connected, Has group, Password
@@ -382,10 +387,12 @@ class View(models.Model):
self = self.sudo()
if self.visibility and not request.env.user.has_group('website.group_website_designer'):
if (self.visibility == 'connected' and request.website.is_public_user()):
visibility = self._get_cached_visibility()
if visibility and not request.env.user.has_group('website.group_website_designer'):
if (visibility == 'connected' and request.website.is_public_user()):
error = werkzeug.exceptions.Forbidden()
elif self.visibility == 'password' and \
elif visibility == 'password' and \
(request.website.is_public_user() or self.id not in request.session.get('views_unlock', [])):
pwd = request.params.get('visibility_password')
if pwd and self.env.user._crypt_context().verify(
@@ -394,7 +401,7 @@ class View(models.Model):
else:
error = werkzeug.exceptions.Forbidden('website_visibility_password_required')
if self.visibility not in ('password', 'connected'):
if visibility not in ('password', 'connected'):
try:
self._check_view_access()
except AccessError:
+1 -1
View File
@@ -119,7 +119,7 @@ class Menu(models.Model):
if menu.page_id and not menu.user_has_groups('base.group_user') and \
(not menu.page_id.sudo().is_visible or
(not menu.page_id.view_id._handle_visibility(do_raise=False) and
menu.page_id.view_id.visibility != "password")):
menu.page_id.view_id._get_cached_visibility() != "password")):
visible = False
menu.is_visible = visible
+1 -1
View File
@@ -106,7 +106,7 @@ class TestWebsitePerformance(UtilPerf):
def test_20_perf_sql_queries_homepage(self):
# homepage "/" has its own controller
self.assertEqual(self._get_url_hot_query('/'), 13)
self.assertEqual(self._get_url_hot_query('/'), 14)
self.assertEqual(self._get_url_hot_query('/', cache=False), 18)
def test_30_perf_sql_queries_page_no_layout(self):
@@ -31,7 +31,7 @@
</p>
</div>
<nav groups="base.group_user" t-if="website" id="oe_main_menu_navbar" class="o_main_navbar d-flex align-items-center">
<div id="oe_applications">
<div t-cache="website" id="oe_applications">
<div class="dropdown active pl-3">
<a class="dropdown-toggle full" data-toggle="dropdown" href="#" accesskey="h">
<i class="fa fa-th-large mr-2"/> WEBSITE
@@ -70,7 +70,7 @@
<a role="menuitem" data-action="promote-current-page" href="#" title="Promote page on the web" class="dropdown-item">Optimize SEO</a>
</div>
</div>
<t t-call="website.website_publisher">
<t t-call="website.website_publisher" groups="website.group_website_publisher">
<t t-set="extra_classes" t-translation="off">d-flex d-sm-flex d-md-none</t>
</t>
</div>
@@ -122,9 +122,8 @@
<p>Page</p>
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_blog')"/>
<div name="module_website_blog" t-if='is_system'
<div name="module_website_blog" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element">
@@ -134,7 +133,7 @@
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_event')"/>
<div name="module_website_event" t-if='is_system'
<div name="module_website_event" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element">
@@ -144,7 +143,7 @@
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_forum')"/>
<div name="module_website_forum" t-if='is_system'
<div name="module_website_forum" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element o_new_content_element_once">
@@ -154,7 +153,7 @@
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_hr_recruitment')"/>
<div name="module_website_hr_recruitment" t-if='is_system'
<div name="module_website_hr_recruitment" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element">
@@ -164,7 +163,7 @@
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_sale')"/>
<div name="module_website_sale" t-if='is_system'
<div name="module_website_sale" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element">
@@ -174,7 +173,7 @@
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_slides')"/>
<div name="module_website_slides" t-if='is_system'
<div name="module_website_slides" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element">
@@ -185,7 +184,7 @@
</a>
</div>
<t t-set="mod" t-value="env.ref('base.module_website_livechat')"/>
<div name="module_website_livechat" t-if='is_system'
<div name="module_website_livechat" t-if='is_system' t-cache="is_system"
t-att-data-module-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element o_new_content_element_once">
+28 -19
View File
@@ -39,15 +39,17 @@
<t t-set="html_data" t-value="{
'lang': lang and lang.replace('_', '-'),
'data-website-id': website.id if website else None,
'data-edit_translations': '1' if edit_translations else None,
}"/>
<t t-if="editable or translatable" t-set="nothing" t-value="html_data.update({
'data-editable': '1' if editable else None,
'data-translatable': '1' if translatable else None,
'data-edit_translations': '1' if edit_translations else None,
'data-view-xmlid': xmlid if editable or translatable else None,
'data-viewid': viewid if editable or translatable else None,
'data-main-object': repr(main_object),
'data-view-xmlid': xmlid,
'data-viewid': viewid,
'data-main-object': repr(main_object) if main_object else None,
'data-seo-object': repr(seo_object) if seo_object else None,
'data-oe-company-name': res_company.name,
}"/>
})"/>
</xpath>
<xpath expr="//head" position="before">
<t t-if="not title">
@@ -70,12 +72,12 @@
<t t-set="website_meta" t-value="seo_object and seo_object.get_website_meta() or {}"/>
<meta name="default_title" t-att-content="default_title" groups="website.group_website_designer"/>
<meta t-if="(main_object and 'website_indexed' in main_object
and not main_object.website_indexed) or (website.domain and website.domain not in request.httprequest.url_root)" name="robots" content="noindex"/>
<t t-set="seo_object" t-value="seo_object or main_object"/>
<t t-set="meta_description" t-value="seo_object and 'website_meta_description' in seo_object
and seo_object.website_meta_description or website_meta_description or website_meta.get('meta_description', '')"/>
<t t-set="meta_keywords" t-value="seo_object and 'website_meta_keywords' in seo_object
and seo_object.website_meta_keywords or website_meta_keywords"/>
and not main_object.website_indexed) or (website.domain and website.domain not in request.httprequest.url_root)" name="robots" content="noindex"/>
<t t-set="seo_object" t-value="seo_object or main_object"/>
<t t-set="meta_description" t-value="seo_object and 'website_meta_description' in seo_object
and seo_object.website_meta_description or website_meta_description or website_meta.get('meta_description', '')"/>
<t t-set="meta_keywords" t-value="seo_object and 'website_meta_keywords' in seo_object
and seo_object.website_meta_keywords or website_meta_keywords"/>
<meta t-if="meta_description or editable" name="description" t-att-content="meta_description"/>
<meta t-if="meta_keywords or editable" name="keywords" t-att-content="meta_keywords"/>
<t t-if="seo_object">
@@ -100,13 +102,15 @@
<t t-foreach="twitter_meta" t-as="t_meta">
<meta t-att-name="t_meta" t-att-content="twitter_meta[t_meta]"/>
</t>
</t>
</t>
<t t-if="request and request.is_frontend_multilang and website">
<t t-set="alternate_languages" t-value="website._get_alternate_languages(canonical_params=canonical_params)"/>
<t t-foreach="alternate_languages" t-as="lg">
<link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/>
<t t-if="request and request.is_frontend_multilang and website">
<t t-cache="website,canonical_params">
<t t-set="alternate_languages" t-value="website._get_alternate_languages(canonical_params=canonical_params)"/>
<t t-foreach="alternate_languages" t-as="lg">
<link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/>
</t>
</t>
</t>
</t>
</t>
<link t-if="request and website" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/>
@@ -124,6 +128,10 @@
<attribute name="data-name">Header</attribute>
</xpath>
<xpath expr="//header/t[@t-cache]" position="attributes">
<attribute name="t-cache">xmlid,website</attribute>
</xpath>
<xpath expr="//header" position="before">
<t t-set="cta_btn_text" t-value="False"/>
<t t-set="cta_btn_href">/contactus</t>
@@ -133,6 +141,7 @@
<attribute name="data-name">Footer</attribute>
<!-- Background now controlled by css configuration, using color combinations -->
<attribute name="t-attf-class" add="o_colored_level o_cc" remove="bg-light" separator=" "/>
<attribute name="t-cache">no_footer,no_copyright,website</attribute>
</xpath>
<xpath expr="//div[hasclass('o_footer_copyright')]" position="attributes">
<attribute name="data-name">Copyright</attribute>
@@ -2004,8 +2013,8 @@
t-att-data-use_size="use_size"
t-att-data-use_filters="use_filters"
t-att-data-use_text_align="use_text_align"
t-att-data-res-model="request.env.user.has_group('website.group_website_publisher') and _name"
t-att-data-res-id="request.env.user.has_group('website.group_website_publisher') and _id"
t-att-data-res-model="_name"
t-att-data-res-id="_id"
t-attf-class="o_record_cover_container d-flex flex-column h-100 o_colored_level o_cc #{_cp.get('background_color_class')} #{use_size and _cp.get('resize_class')} #{use_text_align and _cp.get('text_align_class')} #{additionnal_classes}">
<div t-attf-class="o_record_cover_component o_record_cover_image #{snippet_autofocus and 'o_we_snippet_autofocus'}" t-attf-style="background-image: #{_bg};"/>
<div t-if="use_filters" t-attf-class="o_record_cover_component o_record_cover_filter oe_black" t-attf-style="opacity: #{_cp.get('opacity', 0.0)};"/>
+8 -8
View File
@@ -7,7 +7,7 @@ import pytz
import babel.dates
from collections import OrderedDict
from odoo import http, fields
from odoo import http, fields, tools
from odoo.addons.http_routing.models.ir_http import slug, unslug
from odoo.addons.website.controllers.main import QueryURL
from odoo.addons.portal.controllers.portal import _build_url_w_params
@@ -129,20 +129,20 @@ class WebsiteBlog(http.Controller):
url_args["date_begin"] = date_begin
url_args["date_end"] = date_end
pager = request.website.pager(
pager = tools.lazy(lambda: request.website.pager(
url=request.httprequest.path.partition('/page/')[0],
total=total,
page=page,
step=self._blog_post_per_page,
url_args=url_args,
)
))
if not blogs:
all_tags = request.env['blog.tag']
else:
all_tags = blogs.all_tags(join=True) if not blog else blogs.all_tags().get(blog.id, request.env['blog.tag'])
tag_category = sorted(all_tags.mapped('category_id'), key=lambda category: category.name.upper())
other_tags = sorted(all_tags.filtered(lambda x: not x.category_id), key=lambda tag: tag.name.upper())
all_tags = tools.lazy(lambda: blogs.all_tags(join=True) if not blog else blogs.all_tags().get(blog.id, request.env['blog.tag']))
tag_category = tools.lazy(lambda: sorted(all_tags.mapped('category_id'), key=lambda category: category.name.upper()))
other_tags = tools.lazy(lambda: sorted(all_tags.filtered(lambda x: not x.category_id), key=lambda tag: tag.name.upper()))
# for performance prefetch the first post with the others
post_ids = (first_post | posts).ids
@@ -155,7 +155,7 @@ class WebsiteBlog(http.Controller):
'first_post': first_post.with_prefetch(post_ids),
'other_tags': other_tags,
'tag_category': tag_category,
'nav_list': self.nav_list(),
'nav_list': self.nav_list,
'tags_list': self.tags_list,
'pager': pager,
'posts': posts.with_prefetch(post_ids),
@@ -182,7 +182,7 @@ class WebsiteBlog(http.Controller):
], type='http', auth="public", website=True, sitemap=True)
def blog(self, blog=None, tag=None, page=1, search=None, **opt):
Blog = request.env['blog.blog']
blogs = Blog.search(request.website.website_domain(), order="create_date asc, id asc")
blogs = tools.lazy(lambda: Blog.search(request.website.website_domain(), order="create_date asc, id asc"))
if not blog and len(blogs) == 1:
return request.redirect('/blog/%s' % slug(blogs[0]), code=302)
@@ -13,8 +13,8 @@ class TestBlogPerformance(UtilPerf):
self.env['website'].search([]).channel_id = False
def test_10_perf_sql_blog_standard_data(self):
self.assertEqual(self._get_url_hot_query('/blog'), 28)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 27)
self.assertEqual(self._get_url_hot_query('/blog'), 14)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 14)
def test_20_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post']
@@ -26,10 +26,10 @@ class TestBlogPerformance(UtilPerf):
for blog_post in blog_posts:
blog_post.tag_ids += blog_tags
blog_tags = blog_tags[:-1]
self.assertEqual(self._get_url_hot_query('/blog'), 28)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 27)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 32)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 31)
self.assertEqual(self._get_url_hot_query('/blog'), 14)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 14)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 28)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 28)
def test_30_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post']
@@ -15,7 +15,7 @@
<a href="/blog" t-attf-class="nav-link font-weight-bold #{(not blog) and 'active'}">All</a>
</li>
<li class="nav-item" t-foreach="blogs" t-as="nav_blog">
<a t-attf-href="/blog/#{slug(nav_blog)}"
<a t-attf-href="/blog/#{slug(nav_blog)}" t-cache="nav_blog,blog and nav_blog.id == blog.id"
t-attf-class="nav-link #{(blog and nav_blog.id == blog.id) and 'active' or ''}">
<b t-field="nav_blog.name"/>
</a>
@@ -98,7 +98,7 @@ according to the enabled options.
<!-- Loop through posts: exclude the first one if already displayed as top banner -->
<t t-foreach="posts" t-as="blog_post">
<!-- Assign 'colWidth': 'col-12' is default for List-View and mobile -->
<div t-attf-class="pb-4 col-12 #{colWidth}">
<div t-attf-class="pb-4 col-12 #{colWidth}" t-cache="blog_post,opt_blog_list_view,opt_blog_readable">
<article t-attf-class="o_wblog_post position-relative #{'card h-100' if opt_blog_cards_design else ''}" name="blog_post">
<!-- List-View Design -->
<t t-if="opt_blog_list_view">
@@ -201,6 +201,7 @@ according to the enabled options.
<!-- ====== Sub-Template: Posts list : Posts Teaser + Tags ============= -->
<template id="post_teaser">
<t t-cache="blog_post,str(active_tag_ids)">
<a t-attf-href="/blog/#{slug(blog_post.blog_id)}/#{slug(blog_post)}" class="text-reset text-decoration-none">
<div t-if="opt_blog_list_view" t-field="blog_post.teaser" class="mt-2 o_wblog_read_text"/>
<div t-else="" t-field="blog_post.teaser" t-attf-class="mt-2 #{opt_blog_readable and 'o_wblog_normalize_font'}"/>
@@ -215,6 +216,7 @@ according to the enabled options.
t-esc="one_tag.name"/>
</t>
</div>
</t>
</template>
@@ -143,7 +143,7 @@ list of filtered posts (by date or tag).
<t t-set="use_text_align" t-value="True"/>
<t t-set="additionnal_classes" t-value="'o_wblog_post_page_cover o_record_has_cover cover_auto'"/>
<div class="container position-relative h-100 d-flex flex-column justify-content-around">
<div class="container position-relative h-100 d-flex flex-column justify-content-around" t-cache="_record">
<div t-attf-class="o_wblog_post_title #{'js_tweet' if opt_blog_post_select_to_tweet else ''} #{'js_comment' if opt_blog_post_select_to_comment else ''}">
<div t-if="not date and not tag and not blog" class="h4 bg-o-color-3 px-2 d-inline-block rounded-sm">Latest</div>
<a t-if="not blog and first_post" t-attf-href="/blog/#{slug(first_post.blog_id)}/#{slug(first_post)}" t-att-title="first_post.name" class="text-white text-decoration-none">
@@ -218,7 +218,6 @@ list of filtered posts (by date or tag).
<t t-set="display_opt_name">Blog Post Cover</t>
<t t-set="additionnal_classes" t-value="'o_wblog_post_page_cover'"/>
<div class="container text-center position-relative h-100 d-flex flex-column flex-grow-1 justify-content-around">
<div t-attf-class="o_wblog_post_title #{opt_blog_post_select_to_tweet and 'js_tweet'} #{opt_blog_post_select_to_comment and 'js_comment'}">
<h1 t-field="blog_post.name" id="o_wblog_post_name" class="o_editable_no_shadow" data-oe-expression="blog_post.name" t-att-data-blog-id="blog_post.id" placeholder="Blog Post Title"/>
@@ -413,7 +412,7 @@ list of filtered posts (by date or tag).
</div>
</t>
<t t-else="">
<div id="o_wblog_next_container" class="d-flex flex-column">
<div id="o_wblog_next_container" class="d-flex flex-column" t-cache="next_post">
<t t-call="website.record_cover">
<t t-set="_record" t-value="next_post"/>
<t t-set="_cp" t-value="json.loads(_record.cover_properties)"/>
@@ -478,6 +477,7 @@ list of filtered posts (by date or tag).
<attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath>
</template>
+4 -3
View File
@@ -19,6 +19,7 @@ from odoo.addons.website.controllers.main import QueryURL
from odoo.http import request
from odoo.osv import expression
from odoo.tools.misc import get_lang
from odoo.tools import lazy
class WebsiteEventController(http.Controller):
@@ -177,13 +178,13 @@ class WebsiteEventController(http.Controller):
def _prepare_event_register_values(self, event, **post):
"""Return the require values to render the template."""
urls = event._get_event_resource_urls()
urls = lazy(event._get_event_resource_urls)
return {
'event': event,
'main_object': event,
'range': range,
'google_url': urls.get('google_url'),
'iCal_url': urls.get('iCal_url'),
'google_url': lazy(lambda: urls.get('google_url')),
'iCal_url': lazy(lambda: urls.get('iCal_url')),
}
def _process_tickets_form(self, event, form_details):
@@ -161,7 +161,7 @@
</div>
<!-- List -->
<div t-foreach="event_ids" t-as="event" t-attf-class=" #{opt_event_size} mb-4">
<a t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register'}" class="text-decoration-none" t-att-data-publish="event.website_published and 'on' or 'off'">
<a t-cache="event if not editable and event.website_published else None" t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register'}" class="text-decoration-none" t-att-data-publish="event.website_published and 'on' or 'off'">
<article t-attf-class="h-100 #{opt_events_list_cards and 'card border-0 shadow-sm'}" itemscope="itemscope" itemtype="http://schema.org/Event">
<div class="h-100 row no-gutters">
<!-- Header -->
@@ -30,7 +30,7 @@
<!-- Event - Description -->
<template id="event_description_full" name="Event Description" track="1">
<t t-call="website_event.event_details">
<section class="bg-200 mt-n5">
<section class="bg-200 mt-n5" t-cache="event if not editable and event.website_published else None">
<div class="container overflow-hidden">
<div class="row no-gutters mt-n4 mb-3">
<!-- Description -->
@@ -8,6 +8,7 @@
<attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath>
</template>
+2 -2
View File
@@ -147,9 +147,9 @@ class WebsiteForum(WebsiteProfile):
url_args['filters'] = filters
if my:
url_args['my'] = my
pager = request.website.pager(url=url, total=question_count, page=page,
pager = tools.lazy(lambda: request.website.pager(url=url, total=question_count, page=page,
step=self._post_per_page, scope=self._post_per_page,
url_args=url_args)
url_args=url_args))
values = self._prepare_user_values(forum=forum, searches=post, header={'ask_hide': not forum.active})
values.update({
@@ -1706,6 +1706,7 @@
<attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath>
</template>
@@ -603,6 +603,7 @@
<attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath>
</template>
@@ -7,7 +7,7 @@
-->
<template id="loader" inherit_id="website.layout" name="Livechat : include loader on Website">
<xpath expr="//div[@id='wrapwrap']" position="after">
<t t-if="website and website.channel_id and not no_livechat">
<t t-if="not no_livechat and website and website.channel_id">
<script>
<t t-call="im_livechat.loader">
<t t-set="info" t-value="website.get_livechat_channel_info()"/>
+15 -2
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
from datetime import datetime
from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_decode, url_encode, url_parse
@@ -19,7 +20,9 @@ from odoo.addons.portal.controllers.portal import _build_url_w_params
from odoo.addons.website.controllers import main
from odoo.addons.website.controllers.form import WebsiteForm
from odoo.osv import expression
from odoo.tools import lazy
from odoo.tools.json import scriptsafe as json_scriptsafe
_logger = logging.getLogger(__name__)
@@ -249,7 +252,12 @@ class WebsiteSale(http.Controller):
keep = QueryURL('/shop', category=category and int(category), search=search, attrib=attrib_list, min_price=min_price, max_price=max_price, order=post.get('order'))
pricelist = website.get_current_pricelist()
now = datetime.timestamp(datetime.now())
pricelist = request.env['product.pricelist'].browse(request.session.get('website_sale_current_pl'))
if not pricelist or request.session.get('website_sale_pricelist_time', 0) < now - 60*60: # test: 1 hour in session
pricelist = website.get_current_pricelist()
request.session['website_sale_pricelist_time'] = now
request.session['website_sale_current_pl'] = pricelist.id
request.update_context(pricelist=pricelist.id, partner=request.env.user.partner_id)
@@ -349,6 +357,9 @@ class WebsiteSale(http.Controller):
layout_mode = 'list'
else:
layout_mode = 'grid'
request.session['website_sale_shop_layout_mode'] = layout_mode
products_prices = lazy(lambda: products._get_sales_prices(pricelist))
values = {
'search': fuzzy_search_term or search,
@@ -361,7 +372,7 @@ class WebsiteSale(http.Controller):
'add_qty': add_qty,
'products': products,
'search_count': product_count, # common for all searchbox
'bins': TableCompute().process(products, ppg, ppr),
'bins': lazy(lambda: TableCompute().process(products, ppg, ppr)),
'ppg': ppg,
'ppr': ppr,
'categories': categs,
@@ -369,6 +380,8 @@ class WebsiteSale(http.Controller):
'keep': keep,
'search_categories_ids': search_categories.ids,
'layout_mode': layout_mode,
'products_prices': products_prices,
'get_product_prices': lambda product: lazy(lambda: products_prices[product.id]),
}
if filter_by_price_enabled:
values['min_price'] = min_price or available_min_price
+28 -21
View File
@@ -1,16 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="header_cart_link" name="Header Cart Link">
<t t-set="website_sale_order" t-value="website.sale_get_order()" />
<t t-set="show_cart" t-value="true"/>
<li t-attf-class="#{_item_class} divider d-none"/> <!-- Make sure the cart and related menus are not folded (see autohideMenu) -->
<li t-attf-class="o_wsale_my_cart align-self-md-start #{not show_cart and 'd-none'} #{_item_class}">
<a href="/shop/cart" t-attf-class="#{_link_class}">
<i t-if="_icon" class="fa fa-shopping-cart"/>
<span t-if="_text">My Cart</span>
<sup class="my_cart_quantity badge badge-primary" t-esc="website_sale_order and website_sale_order.cart_quantity or '0'" t-att-data-order-id="website_sale_order and website_sale_order.id or ''"/>
</a>
</li>
<t t-nocache="The number of products is dynamic, this rendering cannot be cached."
t-nocache-_icon="_icon"
t-nocache-_item_class="_item_class"
t-nocache-_link_class="_link_class">
<t t-set="website_sale_order" t-value="website.sale_get_order()" />
<t t-set="show_cart" t-value="true"/>
<li t-attf-class="#{_item_class} divider d-none"/> <!-- Make sure the cart and related menus are not folded (see autohideMenu) -->
<li t-attf-class="o_wsale_my_cart align-self-md-start #{not show_cart and 'd-none'} #{_item_class}">
<a href="/shop/cart" t-attf-class="#{_link_class}">
<i t-if="_icon" class="fa fa-shopping-cart"/>
<span t-if="_text">My Cart</span>
<sup class="my_cart_quantity badge badge-primary" t-esc="website_sale_order and website_sale_order.cart_quantity or '0'" t-att-data-order-id="website_sale_order and website_sale_order.id or ''"/>
</a>
</li>
</t>
</template>
<template id="header_hide_empty_cart_link" inherit_id="website_sale.header_cart_link" name="Header Hide Empty Cart link" active="False">
@@ -156,14 +161,15 @@
</template>
<template id="products_item" name="Products item">
<t t-set="product_href" t-value="keep(product.website_url, page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))" />
<t t-set="image_type" t-value="product._get_suitable_image_size(ppr, td_product['x'], td_product['y'])"/>
<form action="/shop/cart/update" method="post" class="oe_product_cart h-100 d-flex"
t-att-data-publish="product.website_published and 'on' or 'off'"
itemscope="itemscope" itemtype="http://schema.org/Product">
<t t-set="product_href" t-value="keep(product.website_url, page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))" />
<t t-set="image_type" t-value="product._get_suitable_image_size(ppr, td_product['x'], td_product['y'])"/>
<div class="oe_product_image position-relative h-100 flex-grow-0 overflow-hidden">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" t-nocache="The csrf token must always be up to date."/>
<a t-att-href="product_href" class="oe_product_image_link d-block h-100 position-relative" itemprop="url" contenteditable="false">
<t t-set="image_holder" t-value="product._get_image_holder()"/>
<span t-field="image_holder.image_1920"
@@ -186,6 +192,7 @@
</h6>
</div>
<div class="o_wsale_product_sub d-flex justify-content-between align-items-end pb-1">
<t t-set="template_price_vals" t-value="get_product_prices(product)"/>
<div class="o_wsale_product_btn"/>
<div class="product_price" itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer">
<t t-if="'base_price' in template_price_vals">
@@ -279,29 +286,27 @@
<t t-set="_classes" t-valuef="w-100 w-md-auto mr-auto mb-2"/>
<t t-set="search" t-value="original_search or search"/>
</t>
<t t-call="website_sale.pricelist_list">
<t t-call="website_sale.pricelist_list" t-cache="pricelist">
<t t-set="_classes" t-valuef="ml-3 mb-2"/>
</t>
</div>
<div t-if="original_search and bins" class="alert alert-warning mt8">
<div t-if="original_search and products" class="alert alert-warning mt8">
No results found for '<span t-esc="original_search"/>'. Showing results for '<span t-esc="search"/>'.
</div>
<t t-if="category">
<t t-set='editor_msg'>Drag building blocks here to customize the header for "<t t-esc='category.name'/>" category.</t>
<div class="mb16" id="category_header" t-att-data-editor-message="editor_msg" t-field="category.website_description"/>
</t>
<div t-if="bins" class="o_wsale_products_grid_table_wrapper">
<div t-cache="pricelist,products" t-if="products" class="o_wsale_products_grid_table_wrapper">
<table class="table table-borderless m-0" t-att-data-ppg="ppg" t-att-data-ppr="ppr" t-att-data-default-sort="website.shop_default_sort" t-att-data-name="grid_block_name">
<colgroup t-ignore="true">
<!-- Force the number of columns (useful when only one row of (x < ppr) products) -->
<col t-foreach="ppr" t-as="p"/>
</colgroup>
<tbody>
<t t-set="products_prices" t-value="products._get_sales_prices(pricelist)"/>
<tr t-foreach="bins" t-as="tr_product">
<t t-foreach="tr_product" t-as="td_product">
<t t-if="td_product">
<t t-set="product" t-value="td_product['product']" />
<!-- We use t-attf-class here to allow easier customization -->
<td t-att-colspan="td_product['x'] != 1 and td_product['x']"
t-att-rowspan="td_product['y'] != 1 and td_product['y']"
@@ -310,7 +315,7 @@
t-att-data-name="product_block_name">
<div t-attf-class="o_wsale_product_grid_wrapper position-relative h-100 o_wsale_product_grid_wrapper_#{td_product['x']}_#{td_product['y']}">
<t t-call="website_sale.products_item">
<t t-set="template_price_vals" t-value="products_prices[product.id]"/>
<t t-set="product" t-value="td_product['product']"/>
</t>
</div>
</td>
@@ -598,7 +603,7 @@
</template>
<template id="product" name="Product" track="1">
<t t-cache="pricelist,product">
<t t-set="combination" t-value="product._get_first_possible_combination()"/>
<t t-set="combination_info" t-value="product._get_combination_info(combination, add_qty=add_qty or 1, pricelist=pricelist)"/>
<t t-set="product_variant" t-value="product.env['product.product'].browse(combination_info['product_id'])"/>
@@ -719,6 +724,7 @@
<div class="oe_structure oe_empty oe_structure_not_nearest mt16" id="oe_structure_website_sale_product_2" data-editor-message="DROP BUILDING BLOCKS HERE TO MAKE THEM AVAILABLE ACROSS ALL PRODUCTS"/>
</div>
</t>
</t>
</template>
<template id="product_custom_text" inherit_id="website_sale.product" active="True" customize_show="True" name="Terms and Conditions" priority="21">
@@ -1992,6 +1998,7 @@
<attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">env.user.has_group('sales_team.group_sale_manager')</attribute>
<attribute name="t-cache"/>
</xpath>
</template>
@@ -2,11 +2,11 @@
<odoo>
<template id="product_wishlist" inherit_id="website_sale_wishlist.product_wishlist">
<xpath expr="//button[hasclass('o_wish_rm')]" position="before">
<small class="text-danger d-md-block" t-if="wish.product_id.product_tmpl_id._is_sold_out()">Temporarily out of stock</small>
<small class="text-danger d-md-block" t-if="wish.product_id.sudo()._is_sold_out()">Temporarily out of stock</small>
</xpath>
<xpath expr="//button[hasclass('o_wish_rm')]" position="after">
<t t-set="notify" t-value="wish.stock_notification"/>
<button groups="base.group_no_one" t-att-data-notify="notify" t-if="notify or wish.product_id.product_tmpl_id._is_sold_out()" type="button" class="btn btn-link o_notify_stock no-decoration">
<button groups="base.group_no_one" t-att-data-notify="notify" t-if="notify or wish.product_id.sudo()._is_sold_out()" type="button" class="btn btn-link o_notify_stock no-decoration">
<small><i t-attf-class="fa #{'fa-check-square-o' if notify else 'fa-square-o'}"></i> Be notified when back in stock</small>
</button>
</xpath>
@@ -52,7 +52,7 @@ tour.register('shop_wishlist', {
},
{
content: "click on Customizable Desk (TEST)",
trigger: '.oe_product_cart a:contains("Customizable Desk (TEST)")',
trigger: '.oe_product_cart a:contains("Customizable Desk")',
},
{
content: "check the first variant is already in wishlist",
@@ -76,7 +76,7 @@ tour.register('shop_wishlist', {
},
{
content: "remove Customizable Desk (TEST)",
trigger: 'tr:contains("Customizable Desk (TEST)") .o_wish_rm:first',
trigger: 'tr:contains("Customizable Desk") .o_wish_rm:first',
},
{
content: "check that wishlist contains 1 item",
@@ -338,6 +338,69 @@ tour.register('shop_wishlist', {
trigger: '#product_detail:not(:has(.o_add_wishlist_dyn))',
run: function () {},
},
// Test if the wishlist button is active or not in /shop
{
content: "Go to '/shop?search=Customizable Desk'",
trigger: 'body',
run: function () {
window.location.href = '/shop?search=Customizable Desk '
},
},
{
content: "Click on the product",
trigger: '.oe_product_image_link img',
},
{
content: "Add the product in the wishlist",
trigger: '#product_option_block .o_add_wishlist_dyn',
},
{
content: "Added into the wishlist",
trigger: '.my_wish_quantity.badge-primary:contains(1)',
run: function () {},
},
{
content: "Go to '/shop",
trigger: '#top_menu_collapse a[href="/shop"]',
},
{
content: "Search the product Customizable Desk'",
trigger: 'form.o_wsale_products_searchbar_form input',
run: function () {
$('form.o_wsale_products_searchbar_form input[name="search"]').val("Customizable Desk");
$('form.o_wsale_products_searchbar_form button').click();
},
},
{
content: "The product is in the wishlist",
trigger: '.oe_product_cart .o_wsale_product_information:has(.o_add_wishlist[disabled])',
run: function () {},
},
{
content: "Go to the wishlist",
trigger: 'a[href="/shop/wishlist"]',
},
{
content: "Remove the product from the wishlist",
trigger: '.o_wish_rm',
},
{
content: "Go to '/shop",
trigger: '#top_menu_collapse a[href="/shop"]',
},
{
content: "Search the product Customizable Desk'",
trigger: 'form.o_wsale_products_searchbar_form input',
run: function () {
$('form.o_wsale_products_searchbar_form input[name="search"]').val("Customizable Desk");
$('form.o_wsale_products_searchbar_form button').click();
},
},
{
content: "The product is not in the wishlist",
trigger: '.oe_product_cart .o_wsale_product_information:not(:has(.o_add_wishlist[disabled]))',
run: function () {},
},
]
);
@@ -3,30 +3,42 @@
<template id="add_to_wishlist" inherit_id="website_sale.products_item" customize_show="True" name="Wishlist Button" priority="20">
<xpath expr="//div[hasclass('o_wsale_product_btn')]" position="inside">
<t t-set="in_wish" t-value="product._is_in_wishlist()"/>
<t t-set="product_variant_id" t-value="product._get_first_possible_variant_id()"/>
<button t-if="product_variant_id" type="button" role="button" class="btn btn-outline-primary bg-white o_add_wishlist" t-att-disabled='in_wish or None' title="Add to Wishlist" t-att-data-product-template-id="product.id" t-att-data-product-product-id="product_variant_id" data-action="o_wishlist"><span class="fa fa-heart" role="img" aria-label="Add to wishlist"></span></button>
<t t-nocache="The wishlist depends on the user and must not be shared with other users. As this rendering is in the loop, the product value is set in the template, we must retain the id of the product on which we are."
t-nocache-product_template_id="product.id">
<t t-set="product" t-value="products.filtered(lambda p: p.id == product_template_id)"/>
<t t-set="in_wish" t-value="product._is_in_wishlist()"/>
<t t-set="product_variant_id" t-value="product._get_first_possible_variant_id()"/>
<button t-if="product_variant_id" type="button" role="button" class="btn btn-outline-primary bg-white o_add_wishlist" t-att-disabled='in_wish or None' title="Add to Wishlist" t-att-data-product-template-id="product.id" t-att-data-product-product-id="product_variant_id" data-action="o_wishlist"><span class="fa fa-heart" role="img" aria-label="Add to wishlist"></span></button>
</t>
</xpath>
</template>
<template id="product_add_to_wishlist" inherit_id="website_sale.product" customize_show="True" name="Wishlist Button" priority="20">
<xpath expr="//div[@id='product_option_block']" position="inside">
<t t-set="product_variant" t-value="product_variant or product._create_first_product_variant()"/>
<t t-set="in_wish" t-value="product_variant and product_variant._is_in_wishlist()"/>
<button t-if="product_variant" type="button" role="button" class="btn btn-link px-0 pr-3 o_add_wishlist_dyn" t-att-disabled='in_wish or None' t-att-data-product-template-id="product.id" t-att-data-product-product-id="product_variant.id" data-action="o_wishlist" title="Add to wishlist"><i class="fa fa-heart-o mr-2" role="img" aria-label="Add to wishlist"/>Add to wishlist</button>
<t t-nocache="The wishlist depends on the user and must not be shared with other users. The product come from the controller.">
<t t-set="product_variant" t-value="product_variant or product._create_first_product_variant()"/>
<t t-set="in_wish" t-value="product_variant and product_variant._is_in_wishlist()"/>
<button t-if="product_variant" type="button" role="button" class="btn btn-link px-0 pr-3 o_add_wishlist_dyn" t-att-disabled='in_wish or None' t-att-data-product-template-id="product.id" t-att-data-product-product-id="product_variant.id" data-action="o_wishlist" title="Add to wishlist"><i class="fa fa-heart-o mr-2" role="img" aria-label="Add to wishlist"/>Add to wishlist</button>
</t>
</xpath>
</template>
<template id="header_wishlist_link" name="Header Wishlist Link">
<t t-set="wishcount" t-value="len(request.env['product.wishlist'].current())"/>
<t t-set="show_wishes" t-value="True"/>
<li t-attf-class="o_wsale_my_wish #{not show_wishes and 'd-none'} #{_item_class}">
<a href="/shop/wishlist" t-attf-class="#{_link_class}">
<i t-if="_icon" class="fa fa-1x fa-heart"/>
<span t-if="_text">Wishlist</span>
<sup t-esc="wishcount" t-attf-class="my_wish_quantity o_animate_blink badge badge-primary"/>
</a>
</li>
<t t-nocache="The wishlist may vary and depends on the user."
t-nocache-_icon="_icon"
t-nocache-_item_class="_item_class"
t-nocache-_link_class="_link_class"
t-nocache-_text="_text">
<t t-set="wishcount" t-value="len(request.env['product.wishlist'].current())"/>
<t t-set="show_wishes" t-value="True"/>
<li t-attf-class="o_wsale_my_wish #{not show_wishes and 'd-none'} #{_item_class}">
<a href="/shop/wishlist" t-attf-class="#{_link_class}">
<i t-if="_icon" class="fa fa-1x fa-heart"/>
<span t-if="_text">Wishlist</span>
<sup t-esc="wishcount" t-attf-class="my_wish_quantity o_animate_blink badge badge-primary"/>
</a>
</li>
</t>
</template>
<template id="header_hide_empty_wishlist_link" inherit_id="website_sale_wishlist.header_wishlist_link">
+13 -14
View File
@@ -295,34 +295,33 @@ class WebsiteSlides(WebsiteProfile):
@http.route('/slides', type='http', auth="public", website=True, sitemap=True)
def slides_channel_home(self, **post):
""" Home page for eLearning platform. Is mainly a container page, does not allow search / filter. """
domain = request.website.website_domain()
channels_all = request.env['slide.channel'].search(domain)
channels_all = tools.lazy(lambda: request.env['slide.channel'].search(request.website.website_domain()))
if not request.env.user._is_public():
#If a course is completed, we don't want to see it in first position but in last
channels_my = channels_all.filtered(lambda channel: channel.is_member).sorted(lambda channel: 0 if channel.completed else channel.completion, reverse=True)[:3]
channels_my = tools.lazy(lambda: channels_all.filtered(lambda channel: channel.is_member).sorted(lambda channel: 0 if channel.completed else channel.completion, reverse=True)[:3])
else:
channels_my = request.env['slide.channel']
channels_popular = channels_all.sorted('total_votes', reverse=True)[:3]
channels_newest = channels_all.sorted('create_date', reverse=True)[:3]
channels_popular = tools.lazy(lambda: channels_all.sorted('total_votes', reverse=True)[:3])
channels_newest = tools.lazy(lambda: channels_all.sorted('create_date', reverse=True)[:3])
achievements = request.env['gamification.badge.user'].sudo().search([('badge_id.is_published', '=', True)], limit=5)
achievements = tools.lazy(lambda: request.env['gamification.badge.user'].sudo().search([('badge_id.is_published', '=', True)], limit=5))
if request.env.user._is_public():
challenges = None
challenges_done = None
else:
challenges = request.env['gamification.challenge'].sudo().search([
challenges = tools.lazy(lambda: request.env['gamification.challenge'].sudo().search([
('challenge_category', '=', 'slides'),
('reward_id.is_published', '=', True)
], order='id asc', limit=5)
challenges_done = request.env['gamification.badge.user'].sudo().search([
], order='id asc', limit=5))
challenges_done = tools.lazy(lambda: request.env['gamification.badge.user'].sudo().search([
('challenge_id', 'in', challenges.ids),
('user_id', '=', request.env.user.id),
('badge_id.is_published', '=', True)
]).mapped('challenge_id')
]).mapped('challenge_id'))
users = request.env['res.users'].sudo().search([
users = tools.lazy(lambda: request.env['res.users'].sudo().search([
('karma', '>', 0),
('website_published', '=', True)], limit=5, order='karma desc')
('website_published', '=', True)], limit=5, order='karma desc'))
render_values = self._slide_render_context_base()
render_values.update(self._prepare_user_values(**post))
@@ -332,7 +331,7 @@ class WebsiteSlides(WebsiteProfile):
'channels_newest': channels_newest,
'achievements': achievements,
'users': users,
'top3_users': self._get_top3_users(),
'top3_users': tools.lazy(self._get_top3_users),
'challenges': challenges,
'challenges_done': challenges_done,
'search_tags': request.env['slide.channel.tag'],
@@ -1330,7 +1329,7 @@ class WebsiteSlides(WebsiteProfile):
if kwargs.get('channel'):
channels = kwargs['channel']
elif kwargs.get('channel_id'):
channels = request.env['slide.channel'].browse(int(kwargs['channel_id']))
channels = tools.lazy(lambda: request.env['slide.channel'].browse(int(kwargs['channel_id'])))
return channels
def _prepare_user_slides_profile(self, user):
@@ -15,10 +15,10 @@ tour.register('course_publisher_standard', {
test: true
}, [{
content: 'eLearning: click on New (top-menu)',
trigger: 'div.o_new_content_menu a'
trigger: '#new-content-menu > a'
}, {
content: 'eLearning: click on New Course',
trigger: 'a:contains("Course")'
trigger: '#o_new_content_menu_choices a:contains("Course")'
}, {
content: 'eLearning: set name',
trigger: 'input[name="name"]',
@@ -15,10 +15,10 @@ tour.register('course_publisher', {
test: true
}, [{
content: 'eLearning: click on New (top-menu)',
trigger: 'div.o_new_content_menu a'
trigger: '#new-content-menu > a'
}, {
content: 'eLearning: click on New Course',
trigger: 'a:contains("Course")'
trigger: '#o_new_content_menu_choices a:contains("Course")'
}, {
content: 'eLearning: set name',
trigger: 'input[name="name"]',
@@ -329,7 +329,7 @@
</template>
<template id='course_card' name="Course Card">
<div t-attf-class="card w-100 o_wslides_course_card mb-4 #{'o_wslides_course_unpublished' if not channel.is_published else ''}">
<div t-attf-class="card w-100 o_wslides_course_card mb-4 #{'o_wslides_course_unpublished' if not channel.is_published else ''}" t-cache="channel if is_public_user else None">
<t t-set="course_image" t-value="website.image_url(channel, 'image_1024')"/>
<t t-set="channel_frontend_tags" t-value="channel.tag_ids.filtered(lambda tag: tag.color)"/>
<a t-attf-href="/slides/#{slug(channel)}" t-title="channel.name">
@@ -86,6 +86,7 @@
<attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">env.user.has_group('website_slides.group_website_slides_officer')</attribute>
<attribute name="t-cache"/>
</xpath>
</template>