[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. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models from odoo import models
from odoo.tools import is_html_empty from odoo.tools import is_html_empty, lazy
class IrQWeb(models.AbstractModel): class IrQWeb(models.AbstractModel):
@@ -15,9 +15,9 @@ class IrQWeb(models.AbstractModel):
irQweb = super()._prepare_frontend_environment(values) irQweb = super()._prepare_frontend_environment(values)
values.update( values.update(
is_html_empty=is_html_empty, is_html_empty=is_html_empty,
languages=[lang for languages=lazy(lambda: [lang for
lang in irQweb.env['res.lang'].get_available() 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: for key in irQweb.env.context:
if key not in values: 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=" "/> <attribute name="t-attf-class" add="#{'o_portal' if is_portal else ''}" separator=" "/>
</xpath> </xpath>
<xpath expr="//div[@id='wrapwrap']/header/img" position="replace"> <xpath expr="//div[@id='wrapwrap']/header/img" position="replace">
<nav class="navbar navbar-expand navbar-light bg-light"> <t t-cache="res_company">
<div class="container"> <nav class="navbar navbar-expand navbar-light bg-light">
<a href="/" class="navbar-brand logo"> <div class="container">
<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 href="/" class="navbar-brand logo">
</a> <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"/>
<ul id="top_menu" class="nav navbar-nav ml-auto"> </a>
<t t-call="portal.placeholder_user_sign_in"> <ul id="top_menu" class="nav navbar-nav ml-auto">
<t t-set="_item_class" t-value="'nav-item'"/> <t t-call="portal.placeholder_user_sign_in">
<t t-set="_link_class" t-value="'nav-link'"/> <t t-set="_item_class" t-value="'nav-item'"/>
</t> <t t-set="_link_class" t-value="'nav-link'"/>
<t t-call="portal.user_dropdown"> </t>
<t t-set="_user_name" t-value="true"/> <t t-call="portal.user_dropdown">
<t t-set="_item_class" t-value="'nav-item dropdown'"/> <t t-set="_user_name" t-value="true"/>
<t t-set="_link_class" t-value="'nav-link'"/> <t t-set="_item_class" t-value="'nav-item dropdown'"/>
<t t-set="_dropdown_menu_class" t-value="'dropdown-menu-right'"/> <t t-set="_link_class" t-value="'nav-link'"/>
</t> <t t-set="_dropdown_menu_class" t-value="'dropdown-menu-right'"/>
</ul> </t>
</div> </ul>
</nav> </div>
</nav>
</t>
</xpath> </xpath>
<xpath expr="//div[@id='wrapwrap']/main/t[@t-out='0']" position="before"> <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"> <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>
<template id="language_selector" name="Language Selector"> <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-nocache="The query strings can change for the same page and the same rendering."
<t t-set="language_selector_visible" t-value="len(languages) &gt; 1"/> t-nocache-no_text="no_text"
<div t-attf-class="js_language_selector #{_div_classes} d-print-none" t-if="language_selector_visible"> t-nocache-_div_classes="_div_classes"
<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"> t-nocache-_btn_class="_btn_class"
<span t-if="not no_text" t-nocache-_dropdown_menu_class="_dropdown_menu_class">
class="align-middle" <t t-set="active_lang" t-value="list(filter(lambda lg : lg[0] == lang, languages))[0]"/>
t-esc="active_lang[2].split('/').pop()"/> <t t-set="language_selector_visible" t-value="len(languages) &gt; 1"/>
</button> <div t-attf-class="js_language_selector #{_div_classes} d-print-none" t-if="language_selector_visible">
<div t-attf-class="dropdown-menu #{_dropdown_menu_class}" role="menu"> <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">
<t t-foreach="languages" t-as="lg"> <span t-if="not no_text"
<a t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang_code=lg[0])" class="align-middle"
t-attf-class="dropdown-item js_change_lang #{active_lang == lg and 'active'}" t-esc="active_lang[2].split('/').pop()"/>
t-att-data-url_code="lg[1]"> </button>
<span t-if="not no_text" t-esc="lg[2].split('/').pop()"/> <div t-attf-class="dropdown-menu #{_dropdown_menu_class}" role="menu">
</a> <t t-foreach="languages" t-as="lg">
</t> <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>
</div> </t>
</template> </template>
<template id="user_dropdown" name="Portal User Dropdown"> <template id="user_dropdown" name="Portal User Dropdown">
<t t-set="is_connected" t-value="not user_id._is_public()"/> <t t-nocache="Each user is different regardless of the page visited."
<li t-if="is_connected" t-attf-class="#{_item_class} o_no_autohide_item"> t-nocache-_icon="_icon"
<a href="#" role="button" data-toggle="dropdown" t-attf-class="dropdown-toggle #{_link_class}"> t-nocache-_icon_class="_icon_class"
<t t-if="_avatar"> t-nocache-_user_name="_user_name"
<t t-set="avatar_source" t-value="image_data_uri(user_id.avatar_256)"/> t-nocache-_user_name_class="_user_name_class"
<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-nocache-_item_class="_item_class"
</t> t-nocache-_link_class="_link_class"
<i t-if="_icon" t-attf-class="fa fa-1x fa-fw fa-user-circle-o #{_icon_class}"/> t-nocache-_dropdown_menu_class="_dropdown_menu_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"/> <t t-set="is_connected" t-value="not user_id._is_public()"/>
</a> <li t-if="is_connected" t-attf-class="#{_item_class} o_no_autohide_item">
<div t-attf-class="dropdown-menu js_usermenu #{_dropdown_menu_class}" role="menu"> <a href="#" role="button" data-toggle="dropdown" t-attf-class="dropdown-toggle #{_link_class}">
<div id="o_logout_divider" class="dropdown-divider"/> <t t-if="_avatar">
<a t-attf-href="/web/session/logout?redirect=/" role="menuitem" id="o_logout" class="dropdown-item">Logout</a> <t t-set="avatar_source" t-value="image_data_uri(user_id.avatar_256)"/>
</div> <img t-att-src="avatar_source" t-attf-class="rounded-circle o_object_fit_cover #{_avatar_class}" width="24" height="24" alt="" loading="eager"/>
</li> </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>
<template id="portal_breadcrumbs" name="Portal Breadcrumbs"> <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"> <template id="user_sign_in" name="User Sign In" inherit_id="portal.placeholder_user_sign_in">
<xpath expr="." position="inside"> <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> <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> </li>
</xpath> </xpath>
+2 -2
View File
@@ -37,7 +37,7 @@
<t t-call-assets="web.assets_frontend" t-js="false"/> <t t-call-assets="web.assets_frontend" t-js="false"/>
</xpath> </xpath>
<xpath expr="//head/script[@id='web.layout.odooscript']" position="after"> <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())"/>; odoo.__session_info__ = <t t-out="json.dumps(request.env['ir.http'].get_frontend_session_info())"/>;
if (!/(^|;\s)tz=/.test(document.cookie)) { if (!/(^|;\s)tz=/.test(document.cookie)) {
const userTZ = Intl.DateTimeFormat().resolvedOptions().timeZone; const userTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -60,7 +60,7 @@
<main> <main>
<t t-out="0"/> <t t-out="0"/>
</main> </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 id="footer"/>
<div t-if="not no_copyright" class="o_footer_copyright"> <div t-if="not no_copyright" class="o_footer_copyright">
<div class="container py-3"> <div class="container py-3">
+12 -10
View File
@@ -5,6 +5,7 @@ from collections import OrderedDict
from odoo import models from odoo import models
from odoo.http import request from odoo.http import request
from odoo.tools import lazy
from odoo.addons.base.models.assetsbundle import AssetsBundle from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.http_routing.models.ir_http import url_for from odoo.addons.http_routing.models.ir_http import url_for
from odoo.osv import expression from odoo.osv import expression
@@ -60,31 +61,32 @@ class IrQWeb(models.AbstractModel):
Website = irQweb.env['website'] Website = irQweb.env['website']
editable = request.website.is_publisher() editable = request.website.is_publisher()
translatable = editable and irQweb.env.context.get('lang') != irQweb.env['ir.http']._get_default_lang().code 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() current_website = Website.get_current_website()
has_group_publisher = irQweb.env.user.has_group('website.group_website_publisher') 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'): 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_current'] = lazy(lambda: current_website.name)
values['multi_website_websites'] = [ values['multi_website_websites'] = lazy(lambda: [
{'website_id': website.id, 'name': website.name, 'domain': website.domain} {'website_id': website.id, 'name': website.name, 'domain': website.domain}
for website in Website.search([('id', '!=', current_website.id)]) for website in Website.search([('id', '!=', current_website.id)])
] ])
cur_company = irQweb.env.company cur_company = irQweb.env.company
values['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name} values['multi_website_companies_current'] = lazy(lambda: {'company_id': cur_company.id, 'name': cur_company.name})
values['multi_website_companies'] = [ values['multi_website_companies'] = lazy(lambda: [
{'company_id': comp.id, 'name': comp.name} {'company_id': comp.id, 'name': comp.name}
for comp in irQweb.env.user.company_ids if comp != cur_company for comp in irQweb.env.user.company_ids if comp != cur_company
] ])
# update values # update values
values.update(dict( values.update(dict(
website=current_website, website=current_website,
is_view_active=current_website.is_view_active, is_view_active=lazy(lambda: current_website.is_view_active),
res_company=request.env['res.company'].browse(current_website._get_cached('company_id')).sudo(), res_company=lazy(request.env['res.company'].browse(current_website._get_cached('company_id')).sudo),
translatable=translatable, translatable=translatable,
editable=editable, editable=editable,
)) ))
@@ -93,7 +95,7 @@ class IrQWeb(models.AbstractModel):
# form editable object, add the backend configuration link # form editable object, add the backend configuration link
if 'main_object' in values and has_group_publisher: if 'main_object' in values and has_group_publisher:
func = getattr(values['main_object'], 'get_backend_menu_id', False) 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 # 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 Archived views are ignored (unless the active_test context is set, but
then the ormcache_context will not work as expected). then the ormcache_context will not work as expected).
""" """
if 'website_id' in self._context and not isinstance(xml_id, int): website_id = self._context.get('website_id')
current_website = self.env['website'].browse(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() domain = ['&', ('key', '=', xml_id)] + current_website.website_domain()
view = self.sudo().search(domain, order='website_id', limit=1) view = self.sudo().search(domain, order='website_id', limit=1)
@@ -372,6 +373,10 @@ class View(models.Model):
return view.id return view.id
return super(View, self.sudo())._get_view_id(xml_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): def _handle_visibility(self, do_raise=True):
""" Check the visibility set on the main view and raise 403 if you should not have access. """ Check the visibility set on the main view and raise 403 if you should not have access.
Order is: Public, Connected, Has group, Password Order is: Public, Connected, Has group, Password
@@ -382,10 +387,12 @@ class View(models.Model):
self = self.sudo() self = self.sudo()
if self.visibility and not request.env.user.has_group('website.group_website_designer'): visibility = self._get_cached_visibility()
if (self.visibility == 'connected' and request.website.is_public_user()):
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() 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', [])): (request.website.is_public_user() or self.id not in request.session.get('views_unlock', [])):
pwd = request.params.get('visibility_password') pwd = request.params.get('visibility_password')
if pwd and self.env.user._crypt_context().verify( if pwd and self.env.user._crypt_context().verify(
@@ -394,7 +401,7 @@ class View(models.Model):
else: else:
error = werkzeug.exceptions.Forbidden('website_visibility_password_required') error = werkzeug.exceptions.Forbidden('website_visibility_password_required')
if self.visibility not in ('password', 'connected'): if visibility not in ('password', 'connected'):
try: try:
self._check_view_access() self._check_view_access()
except AccessError: 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 \ 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.sudo().is_visible or
(not menu.page_id.view_id._handle_visibility(do_raise=False) and (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 visible = False
menu.is_visible = visible menu.is_visible = visible
+1 -1
View File
@@ -106,7 +106,7 @@ class TestWebsitePerformance(UtilPerf):
def test_20_perf_sql_queries_homepage(self): def test_20_perf_sql_queries_homepage(self):
# homepage "/" has its own controller # 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) self.assertEqual(self._get_url_hot_query('/', cache=False), 18)
def test_30_perf_sql_queries_page_no_layout(self): def test_30_perf_sql_queries_page_no_layout(self):
@@ -31,7 +31,7 @@
</p> </p>
</div> </div>
<nav groups="base.group_user" t-if="website" id="oe_main_menu_navbar" class="o_main_navbar d-flex align-items-center"> <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"> <div class="dropdown active pl-3">
<a class="dropdown-toggle full" data-toggle="dropdown" href="#" accesskey="h"> <a class="dropdown-toggle full" data-toggle="dropdown" href="#" accesskey="h">
<i class="fa fa-th-large mr-2"/> WEBSITE <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> <a role="menuitem" data-action="promote-current-page" href="#" title="Promote page on the web" class="dropdown-item">Optimize SEO</a>
</div> </div>
</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 t-set="extra_classes" t-translation="off">d-flex d-sm-flex d-md-none</t>
</t> </t>
</div> </div>
@@ -122,9 +122,8 @@
<p>Page</p> <p>Page</p>
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_blog')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element"> class="col-md-4 mb8 o_new_content_element">
@@ -134,7 +133,7 @@
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_event')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element"> class="col-md-4 mb8 o_new_content_element">
@@ -144,7 +143,7 @@
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_forum')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element o_new_content_element_once"> class="col-md-4 mb8 o_new_content_element o_new_content_element_once">
@@ -154,7 +153,7 @@
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_hr_recruitment')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element"> class="col-md-4 mb8 o_new_content_element">
@@ -164,7 +163,7 @@
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_sale')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element"> class="col-md-4 mb8 o_new_content_element">
@@ -174,7 +173,7 @@
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_slides')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element"> class="col-md-4 mb8 o_new_content_element">
@@ -185,7 +184,7 @@
</a> </a>
</div> </div>
<t t-set="mod" t-value="env.ref('base.module_website_livechat')"/> <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-id="mod.id"
t-att-data-module-shortdesc="mod.shortdesc" t-att-data-module-shortdesc="mod.shortdesc"
class="col-md-4 mb8 o_new_content_element o_new_content_element_once"> 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="{ <t t-set="html_data" t-value="{
'lang': lang and lang.replace('_', '-'), 'lang': lang and lang.replace('_', '-'),
'data-website-id': website.id if website else None, '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-editable': '1' if editable else None,
'data-translatable': '1' if translatable else None, 'data-translatable': '1' if translatable else None,
'data-edit_translations': '1' if edit_translations else None, 'data-view-xmlid': xmlid,
'data-view-xmlid': xmlid if editable or translatable else None, 'data-viewid': viewid,
'data-viewid': viewid if editable or translatable else None, 'data-main-object': repr(main_object) if main_object else None,
'data-main-object': repr(main_object),
'data-seo-object': repr(seo_object) if seo_object else None, 'data-seo-object': repr(seo_object) if seo_object else None,
'data-oe-company-name': res_company.name, 'data-oe-company-name': res_company.name,
}"/> })"/>
</xpath> </xpath>
<xpath expr="//head" position="before"> <xpath expr="//head" position="before">
<t t-if="not title"> <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 {}"/> <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 name="default_title" t-att-content="default_title" groups="website.group_website_designer"/>
<meta t-if="(main_object and 'website_indexed' in main_object <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"/> 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="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 <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', '')"/> 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 <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 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_description or editable" name="description" t-att-content="meta_description"/>
<meta t-if="meta_keywords or editable" name="keywords" t-att-content="meta_keywords"/> <meta t-if="meta_keywords or editable" name="keywords" t-att-content="meta_keywords"/>
<t t-if="seo_object"> <t t-if="seo_object">
@@ -100,13 +102,15 @@
<t t-foreach="twitter_meta" t-as="t_meta"> <t t-foreach="twitter_meta" t-as="t_meta">
<meta t-att-name="t_meta" t-att-content="twitter_meta[t_meta]"/> <meta t-att-name="t_meta" t-att-content="twitter_meta[t_meta]"/>
</t> </t>
</t>
</t>
<t t-if="request and request.is_frontend_multilang and website"> <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-cache="website,canonical_params">
<t t-foreach="alternate_languages" t-as="lg"> <t t-set="alternate_languages" t-value="website._get_alternate_languages(canonical_params=canonical_params)"/>
<link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/> <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>
</t> </t>
<link t-if="request and website" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/> <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> <attribute name="data-name">Header</attribute>
</xpath> </xpath>
<xpath expr="//header/t[@t-cache]" position="attributes">
<attribute name="t-cache">xmlid,website</attribute>
</xpath>
<xpath expr="//header" position="before"> <xpath expr="//header" position="before">
<t t-set="cta_btn_text" t-value="False"/> <t t-set="cta_btn_text" t-value="False"/>
<t t-set="cta_btn_href">/contactus</t> <t t-set="cta_btn_href">/contactus</t>
@@ -133,6 +141,7 @@
<attribute name="data-name">Footer</attribute> <attribute name="data-name">Footer</attribute>
<!-- Background now controlled by css configuration, using color combinations --> <!-- 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-attf-class" add="o_colored_level o_cc" remove="bg-light" separator=" "/>
<attribute name="t-cache">no_footer,no_copyright,website</attribute>
</xpath> </xpath>
<xpath expr="//div[hasclass('o_footer_copyright')]" position="attributes"> <xpath expr="//div[hasclass('o_footer_copyright')]" position="attributes">
<attribute name="data-name">Copyright</attribute> <attribute name="data-name">Copyright</attribute>
@@ -2004,8 +2013,8 @@
t-att-data-use_size="use_size" t-att-data-use_size="use_size"
t-att-data-use_filters="use_filters" t-att-data-use_filters="use_filters"
t-att-data-use_text_align="use_text_align" 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-model="_name"
t-att-data-res-id="request.env.user.has_group('website.group_website_publisher') and _id" 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}"> 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-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)};"/> <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 import babel.dates
from collections import OrderedDict 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.http_routing.models.ir_http import slug, unslug
from odoo.addons.website.controllers.main import QueryURL from odoo.addons.website.controllers.main import QueryURL
from odoo.addons.portal.controllers.portal import _build_url_w_params 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_begin"] = date_begin
url_args["date_end"] = date_end url_args["date_end"] = date_end
pager = request.website.pager( pager = tools.lazy(lambda: request.website.pager(
url=request.httprequest.path.partition('/page/')[0], url=request.httprequest.path.partition('/page/')[0],
total=total, total=total,
page=page, page=page,
step=self._blog_post_per_page, step=self._blog_post_per_page,
url_args=url_args, url_args=url_args,
) ))
if not blogs: if not blogs:
all_tags = request.env['blog.tag'] all_tags = request.env['blog.tag']
else: else:
all_tags = blogs.all_tags(join=True) if not blog else blogs.all_tags().get(blog.id, request.env['blog.tag']) 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 = sorted(all_tags.mapped('category_id'), key=lambda category: category.name.upper()) tag_category = tools.lazy(lambda: 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()) 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 # for performance prefetch the first post with the others
post_ids = (first_post | posts).ids post_ids = (first_post | posts).ids
@@ -155,7 +155,7 @@ class WebsiteBlog(http.Controller):
'first_post': first_post.with_prefetch(post_ids), 'first_post': first_post.with_prefetch(post_ids),
'other_tags': other_tags, 'other_tags': other_tags,
'tag_category': tag_category, 'tag_category': tag_category,
'nav_list': self.nav_list(), 'nav_list': self.nav_list,
'tags_list': self.tags_list, 'tags_list': self.tags_list,
'pager': pager, 'pager': pager,
'posts': posts.with_prefetch(post_ids), 'posts': posts.with_prefetch(post_ids),
@@ -182,7 +182,7 @@ class WebsiteBlog(http.Controller):
], type='http', auth="public", website=True, sitemap=True) ], type='http', auth="public", website=True, sitemap=True)
def blog(self, blog=None, tag=None, page=1, search=None, **opt): def blog(self, blog=None, tag=None, page=1, search=None, **opt):
Blog = request.env['blog.blog'] 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: if not blog and len(blogs) == 1:
return request.redirect('/blog/%s' % slug(blogs[0]), code=302) return request.redirect('/blog/%s' % slug(blogs[0]), code=302)
@@ -13,8 +13,8 @@ class TestBlogPerformance(UtilPerf):
self.env['website'].search([]).channel_id = False self.env['website'].search([]).channel_id = False
def test_10_perf_sql_blog_standard_data(self): 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'), 14)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 27) self.assertEqual(self._get_url_hot_query('/blog', cache=False), 14)
def test_20_perf_sql_blog_bigger_data_scaling(self): def test_20_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post'] BlogPost = self.env['blog.post']
@@ -26,10 +26,10 @@ class TestBlogPerformance(UtilPerf):
for blog_post in blog_posts: for blog_post in blog_posts:
blog_post.tag_ids += blog_tags blog_post.tag_ids += blog_tags
blog_tags = blog_tags[:-1] blog_tags = blog_tags[:-1]
self.assertEqual(self._get_url_hot_query('/blog'), 28) self.assertEqual(self._get_url_hot_query('/blog'), 14)
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 27) self.assertEqual(self._get_url_hot_query('/blog', cache=False), 14)
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), 28)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 31) 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): def test_30_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post'] 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> <a href="/blog" t-attf-class="nav-link font-weight-bold #{(not blog) and 'active'}">All</a>
</li> </li>
<li class="nav-item" t-foreach="blogs" t-as="nav_blog"> <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 ''}"> t-attf-class="nav-link #{(blog and nav_blog.id == blog.id) and 'active' or ''}">
<b t-field="nav_blog.name"/> <b t-field="nav_blog.name"/>
</a> </a>
@@ -98,7 +98,7 @@ according to the enabled options.
<!-- Loop through posts: exclude the first one if already displayed as top banner --> <!-- Loop through posts: exclude the first one if already displayed as top banner -->
<t t-foreach="posts" t-as="blog_post"> <t t-foreach="posts" t-as="blog_post">
<!-- Assign 'colWidth': 'col-12' is default for List-View and mobile --> <!-- 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"> <article t-attf-class="o_wblog_post position-relative #{'card h-100' if opt_blog_cards_design else ''}" name="blog_post">
<!-- List-View Design --> <!-- List-View Design -->
<t t-if="opt_blog_list_view"> <t t-if="opt_blog_list_view">
@@ -201,6 +201,7 @@ according to the enabled options.
<!-- ====== Sub-Template: Posts list : Posts Teaser + Tags ============= --> <!-- ====== Sub-Template: Posts list : Posts Teaser + Tags ============= -->
<template id="post_teaser"> <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"> <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-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'}"/> <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-esc="one_tag.name"/>
</t> </t>
</div> </div>
</t>
</template> </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="use_text_align" t-value="True"/>
<t t-set="additionnal_classes" t-value="'o_wblog_post_page_cover o_record_has_cover cover_auto'"/> <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-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> <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"> <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="display_opt_name">Blog Post Cover</t>
<t t-set="additionnal_classes" t-value="'o_wblog_post_page_cover'"/> <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 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'}"> <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"/> <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> </div>
</t> </t>
<t t-else=""> <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-call="website.record_cover">
<t t-set="_record" t-value="next_post"/> <t t-set="_record" t-value="next_post"/>
<t t-set="_cp" t-value="json.loads(_record.cover_properties)"/> <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-id"/>
<attribute name="t-att-data-module-shortdesc"/> <attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute> <attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath> </xpath>
</template> </template>
+4 -3
View File
@@ -19,6 +19,7 @@ from odoo.addons.website.controllers.main import QueryURL
from odoo.http import request from odoo.http import request
from odoo.osv import expression from odoo.osv import expression
from odoo.tools.misc import get_lang from odoo.tools.misc import get_lang
from odoo.tools import lazy
class WebsiteEventController(http.Controller): class WebsiteEventController(http.Controller):
@@ -177,13 +178,13 @@ class WebsiteEventController(http.Controller):
def _prepare_event_register_values(self, event, **post): def _prepare_event_register_values(self, event, **post):
"""Return the require values to render the template.""" """Return the require values to render the template."""
urls = event._get_event_resource_urls() urls = lazy(event._get_event_resource_urls)
return { return {
'event': event, 'event': event,
'main_object': event, 'main_object': event,
'range': range, 'range': range,
'google_url': urls.get('google_url'), 'google_url': lazy(lambda: urls.get('google_url')),
'iCal_url': urls.get('iCal_url'), 'iCal_url': lazy(lambda: urls.get('iCal_url')),
} }
def _process_tickets_form(self, event, form_details): def _process_tickets_form(self, event, form_details):
@@ -161,7 +161,7 @@
</div> </div>
<!-- List --> <!-- List -->
<div t-foreach="event_ids" t-as="event" t-attf-class=" #{opt_event_size} mb-4"> <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"> <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"> <div class="h-100 row no-gutters">
<!-- Header --> <!-- Header -->
@@ -30,7 +30,7 @@
<!-- Event - Description --> <!-- Event - Description -->
<template id="event_description_full" name="Event Description" track="1"> <template id="event_description_full" name="Event Description" track="1">
<t t-call="website_event.event_details"> <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="container overflow-hidden">
<div class="row no-gutters mt-n4 mb-3"> <div class="row no-gutters mt-n4 mb-3">
<!-- Description --> <!-- Description -->
@@ -8,6 +8,7 @@
<attribute name="t-att-data-module-id"/> <attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/> <attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute> <attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath> </xpath>
</template> </template>
+2 -2
View File
@@ -147,9 +147,9 @@ class WebsiteForum(WebsiteProfile):
url_args['filters'] = filters url_args['filters'] = filters
if my: if my:
url_args['my'] = 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, 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 = self._prepare_user_values(forum=forum, searches=post, header={'ask_hide': not forum.active})
values.update({ values.update({
@@ -1706,6 +1706,7 @@
<attribute name="t-att-data-module-id"/> <attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/> <attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute> <attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath> </xpath>
</template> </template>
@@ -603,6 +603,7 @@
<attribute name="t-att-data-module-id"/> <attribute name="t-att-data-module-id"/>
<attribute name="t-att-data-module-shortdesc"/> <attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">is_designer</attribute> <attribute name="t-if">is_designer</attribute>
<attribute name="t-cache"/>
</xpath> </xpath>
</template> </template>
@@ -7,7 +7,7 @@
--> -->
<template id="loader" inherit_id="website.layout" name="Livechat : include loader on Website"> <template id="loader" inherit_id="website.layout" name="Livechat : include loader on Website">
<xpath expr="//div[@id='wrapwrap']" position="after"> <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> <script>
<t t-call="im_livechat.loader"> <t t-call="im_livechat.loader">
<t t-set="info" t-value="website.get_livechat_channel_info()"/> <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. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import json import json
import logging import logging
from datetime import datetime
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_decode, url_encode, url_parse 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 import main
from odoo.addons.website.controllers.form import WebsiteForm from odoo.addons.website.controllers.form import WebsiteForm
from odoo.osv import expression from odoo.osv import expression
from odoo.tools import lazy
from odoo.tools.json import scriptsafe as json_scriptsafe from odoo.tools.json import scriptsafe as json_scriptsafe
_logger = logging.getLogger(__name__) _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')) 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) request.update_context(pricelist=pricelist.id, partner=request.env.user.partner_id)
@@ -349,6 +357,9 @@ class WebsiteSale(http.Controller):
layout_mode = 'list' layout_mode = 'list'
else: else:
layout_mode = 'grid' layout_mode = 'grid'
request.session['website_sale_shop_layout_mode'] = layout_mode
products_prices = lazy(lambda: products._get_sales_prices(pricelist))
values = { values = {
'search': fuzzy_search_term or search, 'search': fuzzy_search_term or search,
@@ -361,7 +372,7 @@ class WebsiteSale(http.Controller):
'add_qty': add_qty, 'add_qty': add_qty,
'products': products, 'products': products,
'search_count': product_count, # common for all searchbox 'search_count': product_count, # common for all searchbox
'bins': TableCompute().process(products, ppg, ppr), 'bins': lazy(lambda: TableCompute().process(products, ppg, ppr)),
'ppg': ppg, 'ppg': ppg,
'ppr': ppr, 'ppr': ppr,
'categories': categs, 'categories': categs,
@@ -369,6 +380,8 @@ class WebsiteSale(http.Controller):
'keep': keep, 'keep': keep,
'search_categories_ids': search_categories.ids, 'search_categories_ids': search_categories.ids,
'layout_mode': layout_mode, 'layout_mode': layout_mode,
'products_prices': products_prices,
'get_product_prices': lambda product: lazy(lambda: products_prices[product.id]),
} }
if filter_by_price_enabled: if filter_by_price_enabled:
values['min_price'] = min_price or available_min_price values['min_price'] = min_price or available_min_price
+28 -21
View File
@@ -1,16 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<template id="header_cart_link" name="Header Cart Link"> <template id="header_cart_link" name="Header Cart Link">
<t t-set="website_sale_order" t-value="website.sale_get_order()" /> <t t-nocache="The number of products is dynamic, this rendering cannot be cached."
<t t-set="show_cart" t-value="true"/> t-nocache-_icon="_icon"
<li t-attf-class="#{_item_class} divider d-none"/> <!-- Make sure the cart and related menus are not folded (see autohideMenu) --> t-nocache-_item_class="_item_class"
<li t-attf-class="o_wsale_my_cart align-self-md-start #{not show_cart and 'd-none'} #{_item_class}"> t-nocache-_link_class="_link_class">
<a href="/shop/cart" t-attf-class="#{_link_class}"> <t t-set="website_sale_order" t-value="website.sale_get_order()" />
<i t-if="_icon" class="fa fa-shopping-cart"/> <t t-set="show_cart" t-value="true"/>
<span t-if="_text">My Cart</span> <li t-attf-class="#{_item_class} divider d-none"/> <!-- Make sure the cart and related menus are not folded (see autohideMenu) -->
<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 ''"/> <li t-attf-class="o_wsale_my_cart align-self-md-start #{not show_cart and 'd-none'} #{_item_class}">
</a> <a href="/shop/cart" t-attf-class="#{_link_class}">
</li> <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>
<template id="header_hide_empty_cart_link" inherit_id="website_sale.header_cart_link" name="Header Hide Empty Cart link" active="False"> <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>
<template id="products_item" name="Products item"> <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" <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'" t-att-data-publish="product.website_published and 'on' or 'off'"
itemscope="itemscope" itemtype="http://schema.org/Product"> 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"> <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"> <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()"/> <t t-set="image_holder" t-value="product._get_image_holder()"/>
<span t-field="image_holder.image_1920" <span t-field="image_holder.image_1920"
@@ -186,6 +192,7 @@
</h6> </h6>
</div> </div>
<div class="o_wsale_product_sub d-flex justify-content-between align-items-end pb-1"> <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="o_wsale_product_btn"/>
<div class="product_price" itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer"> <div class="product_price" itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer">
<t t-if="'base_price' in template_price_vals"> <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="_classes" t-valuef="w-100 w-md-auto mr-auto mb-2"/>
<t t-set="search" t-value="original_search or search"/> <t t-set="search" t-value="original_search or search"/>
</t> </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 t-set="_classes" t-valuef="ml-3 mb-2"/>
</t> </t>
</div> </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"/>'. No results found for '<span t-esc="original_search"/>'. Showing results for '<span t-esc="search"/>'.
</div> </div>
<t t-if="category"> <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> <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"/> <div class="mb16" id="category_header" t-att-data-editor-message="editor_msg" t-field="category.website_description"/>
</t> </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"> <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"> <colgroup t-ignore="true">
<!-- Force the number of columns (useful when only one row of (x < ppr) products) --> <!-- Force the number of columns (useful when only one row of (x < ppr) products) -->
<col t-foreach="ppr" t-as="p"/> <col t-foreach="ppr" t-as="p"/>
</colgroup> </colgroup>
<tbody> <tbody>
<t t-set="products_prices" t-value="products._get_sales_prices(pricelist)"/>
<tr t-foreach="bins" t-as="tr_product"> <tr t-foreach="bins" t-as="tr_product">
<t t-foreach="tr_product" t-as="td_product"> <t t-foreach="tr_product" t-as="td_product">
<t t-if="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 --> <!-- We use t-attf-class here to allow easier customization -->
<td t-att-colspan="td_product['x'] != 1 and td_product['x']" <td t-att-colspan="td_product['x'] != 1 and td_product['x']"
t-att-rowspan="td_product['y'] != 1 and td_product['y']" t-att-rowspan="td_product['y'] != 1 and td_product['y']"
@@ -310,7 +315,7 @@
t-att-data-name="product_block_name"> 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']}"> <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-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> </t>
</div> </div>
</td> </td>
@@ -598,7 +603,7 @@
</template> </template>
<template id="product" name="Product" track="1"> <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" 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="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'])"/> <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 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> </div>
</t> </t>
</t>
</template> </template>
<template id="product_custom_text" inherit_id="website_sale.product" active="True" customize_show="True" name="Terms and Conditions" priority="21"> <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-id"/>
<attribute name="t-att-data-module-shortdesc"/> <attribute name="t-att-data-module-shortdesc"/>
<attribute name="t-if">env.user.has_group('sales_team.group_sale_manager')</attribute> <attribute name="t-if">env.user.has_group('sales_team.group_sale_manager')</attribute>
<attribute name="t-cache"/>
</xpath> </xpath>
</template> </template>
@@ -2,11 +2,11 @@
<odoo> <odoo>
<template id="product_wishlist" inherit_id="website_sale_wishlist.product_wishlist"> <template id="product_wishlist" inherit_id="website_sale_wishlist.product_wishlist">
<xpath expr="//button[hasclass('o_wish_rm')]" position="before"> <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>
<xpath expr="//button[hasclass('o_wish_rm')]" position="after"> <xpath expr="//button[hasclass('o_wish_rm')]" position="after">
<t t-set="notify" t-value="wish.stock_notification"/> <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> <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> </button>
</xpath> </xpath>
@@ -52,7 +52,7 @@ tour.register('shop_wishlist', {
}, },
{ {
content: "click on Customizable Desk (TEST)", 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", content: "check the first variant is already in wishlist",
@@ -76,7 +76,7 @@ tour.register('shop_wishlist', {
}, },
{ {
content: "remove Customizable Desk (TEST)", 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", content: "check that wishlist contains 1 item",
@@ -338,6 +338,69 @@ tour.register('shop_wishlist', {
trigger: '#product_detail:not(:has(.o_add_wishlist_dyn))', trigger: '#product_detail:not(:has(.o_add_wishlist_dyn))',
run: function () {}, 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"> <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"> <xpath expr="//div[hasclass('o_wsale_product_btn')]" position="inside">
<t t-set="in_wish" t-value="product._is_in_wishlist()"/> <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 t-set="product_variant_id" t-value="product._get_first_possible_variant_id()"/> t-nocache-product_template_id="product.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-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> </xpath>
</template> </template>
<template id="product_add_to_wishlist" inherit_id="website_sale.product" customize_show="True" name="Wishlist Button" priority="20"> <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"> <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-nocache="The wishlist depends on the user and must not be shared with other users. The product come from the controller.">
<t t-set="in_wish" t-value="product_variant and product_variant._is_in_wishlist()"/> <t t-set="product_variant" t-value="product_variant or product._create_first_product_variant()"/>
<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-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> </xpath>
</template> </template>
<template id="header_wishlist_link" name="Header Wishlist Link"> <template id="header_wishlist_link" name="Header Wishlist Link">
<t t-set="wishcount" t-value="len(request.env['product.wishlist'].current())"/> <t t-nocache="The wishlist may vary and depends on the user."
<t t-set="show_wishes" t-value="True"/> t-nocache-_icon="_icon"
<li t-attf-class="o_wsale_my_wish #{not show_wishes and 'd-none'} #{_item_class}"> t-nocache-_item_class="_item_class"
<a href="/shop/wishlist" t-attf-class="#{_link_class}"> t-nocache-_link_class="_link_class"
<i t-if="_icon" class="fa fa-1x fa-heart"/> t-nocache-_text="_text">
<span t-if="_text">Wishlist</span> <t t-set="wishcount" t-value="len(request.env['product.wishlist'].current())"/>
<sup t-esc="wishcount" t-attf-class="my_wish_quantity o_animate_blink badge badge-primary"/> <t t-set="show_wishes" t-value="True"/>
</a> <li t-attf-class="o_wsale_my_wish #{not show_wishes and 'd-none'} #{_item_class}">
</li> <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>
<template id="header_hide_empty_wishlist_link" inherit_id="website_sale_wishlist.header_wishlist_link"> <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) @http.route('/slides', type='http', auth="public", website=True, sitemap=True)
def slides_channel_home(self, **post): def slides_channel_home(self, **post):
""" Home page for eLearning platform. Is mainly a container page, does not allow search / filter. """ """ Home page for eLearning platform. Is mainly a container page, does not allow search / filter. """
domain = request.website.website_domain() channels_all = tools.lazy(lambda: request.env['slide.channel'].search(request.website.website_domain()))
channels_all = request.env['slide.channel'].search(domain)
if not request.env.user._is_public(): 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 #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: else:
channels_my = request.env['slide.channel'] channels_my = request.env['slide.channel']
channels_popular = channels_all.sorted('total_votes', reverse=True)[:3] channels_popular = tools.lazy(lambda: channels_all.sorted('total_votes', reverse=True)[:3])
channels_newest = channels_all.sorted('create_date', 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(): if request.env.user._is_public():
challenges = None challenges = None
challenges_done = None challenges_done = None
else: else:
challenges = request.env['gamification.challenge'].sudo().search([ challenges = tools.lazy(lambda: request.env['gamification.challenge'].sudo().search([
('challenge_category', '=', 'slides'), ('challenge_category', '=', 'slides'),
('reward_id.is_published', '=', True) ('reward_id.is_published', '=', True)
], order='id asc', limit=5) ], order='id asc', limit=5))
challenges_done = request.env['gamification.badge.user'].sudo().search([ challenges_done = tools.lazy(lambda: request.env['gamification.badge.user'].sudo().search([
('challenge_id', 'in', challenges.ids), ('challenge_id', 'in', challenges.ids),
('user_id', '=', request.env.user.id), ('user_id', '=', request.env.user.id),
('badge_id.is_published', '=', True) ('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), ('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 = self._slide_render_context_base()
render_values.update(self._prepare_user_values(**post)) render_values.update(self._prepare_user_values(**post))
@@ -332,7 +331,7 @@ class WebsiteSlides(WebsiteProfile):
'channels_newest': channels_newest, 'channels_newest': channels_newest,
'achievements': achievements, 'achievements': achievements,
'users': users, 'users': users,
'top3_users': self._get_top3_users(), 'top3_users': tools.lazy(self._get_top3_users),
'challenges': challenges, 'challenges': challenges,
'challenges_done': challenges_done, 'challenges_done': challenges_done,
'search_tags': request.env['slide.channel.tag'], 'search_tags': request.env['slide.channel.tag'],
@@ -1330,7 +1329,7 @@ class WebsiteSlides(WebsiteProfile):
if kwargs.get('channel'): if kwargs.get('channel'):
channels = kwargs['channel'] channels = kwargs['channel']
elif kwargs.get('channel_id'): 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 return channels
def _prepare_user_slides_profile(self, user): def _prepare_user_slides_profile(self, user):
@@ -15,10 +15,10 @@ tour.register('course_publisher_standard', {
test: true test: true
}, [{ }, [{
content: 'eLearning: click on New (top-menu)', 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', content: 'eLearning: click on New Course',
trigger: 'a:contains("Course")' trigger: '#o_new_content_menu_choices a:contains("Course")'
}, { }, {
content: 'eLearning: set name', content: 'eLearning: set name',
trigger: 'input[name="name"]', trigger: 'input[name="name"]',
@@ -15,10 +15,10 @@ tour.register('course_publisher', {
test: true test: true
}, [{ }, [{
content: 'eLearning: click on New (top-menu)', 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', content: 'eLearning: click on New Course',
trigger: 'a:contains("Course")' trigger: '#o_new_content_menu_choices a:contains("Course")'
}, { }, {
content: 'eLearning: set name', content: 'eLearning: set name',
trigger: 'input[name="name"]', trigger: 'input[name="name"]',
@@ -329,7 +329,7 @@
</template> </template>
<template id='course_card' name="Course Card"> <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="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)"/> <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"> <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-id"/>
<attribute name="t-att-data-module-shortdesc"/> <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-if">env.user.has_group('website_slides.group_website_slides_officer')</attribute>
<attribute name="t-cache"/>
</xpath> </xpath>
</template> </template>