[IMP] website, website_sale: Improve website tracking
The update of visit_count now uses website_track instead of website_visitor_lastconnection for less complex query and better precision. The name of a visitor is now changed only if it has no partner linked and the # is removed The cookie was not reset when a new partner connects or if a partner disconnects. Now a check is performed to ensure the cookie belongs to the partner/public user. Now website visitors that log in will be added to the previous website visitor of the partner. This way we have a full history and only one visitor per partner. Now a partner is able to delete a recently viewed product from his recently viewed product list. task-2072877
This commit is contained in:
@@ -63,9 +63,16 @@ class ResUsers(models.Model):
|
||||
env = api.Environment(cr, uid, {})
|
||||
visitor_sudo = env['website.visitor']._get_visitor_from_request()
|
||||
if visitor_sudo:
|
||||
vals = {
|
||||
'user_partner_id': env.user.partner_id.id,
|
||||
'name': env.user.partner_id.name
|
||||
}
|
||||
visitor_sudo.write(vals)
|
||||
partner = env.user.partner_id
|
||||
partner_visitor = env['website.visitor'].sudo().search([('user_partner_id', '=', partner.id)])
|
||||
if partner_visitor and partner_visitor.id != visitor_sudo.id:
|
||||
tracks = visitor_sudo.website_track_ids
|
||||
tracks.write({'visitor_id': partner_visitor.id})
|
||||
visitor_sudo.unlink()
|
||||
else:
|
||||
vals = {
|
||||
'user_partner_id': partner.id,
|
||||
'name': partner.name
|
||||
}
|
||||
visitor_sudo.write(vals)
|
||||
return uid
|
||||
|
||||
@@ -7,7 +7,7 @@ import uuid
|
||||
from odoo import fields, models, api, registry, _
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import _format_time_ago, format_time
|
||||
from odoo.tools.misc import _format_time_ago
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
|
||||
@@ -23,6 +23,7 @@ class WebsiteTrack(models.Model):
|
||||
url = fields.Text('Url', index=True)
|
||||
visit_datetime = fields.Datetime('Visit Date', default=fields.Datetime.now, required=True, readonly=True)
|
||||
|
||||
|
||||
class WebsiteVisitor(models.Model):
|
||||
_name = 'website.visitor'
|
||||
_description = 'Website Visitor'
|
||||
@@ -57,6 +58,11 @@ class WebsiteVisitor(models.Model):
|
||||
time_since_last_action = fields.Char('Last action', compute="_compute_time_statistics", help='Time since last page view. E.g.: 2 minutes ago')
|
||||
is_connected = fields.Boolean('Is connected ?', compute='_compute_time_statistics', help='A visitor is considered as connected if his last page view was within the last 5 minutes.')
|
||||
|
||||
_sql_constraints = [
|
||||
('access_token_unique', 'unique(access_token)', 'Access token should be unique.'),
|
||||
('partner_uniq', 'unique(user_partner_id)', 'A partner is linked to only one visitor.'),
|
||||
]
|
||||
|
||||
@api.depends('name')
|
||||
def name_get(self):
|
||||
return [(
|
||||
@@ -95,7 +101,7 @@ class WebsiteVisitor(models.Model):
|
||||
mapped_data[result['visitor_id'][0]] = visitor_info
|
||||
|
||||
for visitor in self:
|
||||
visitor_info = mapped_data.get(visitor.id, {'page_ids': [], 'page_count': 0})
|
||||
visitor_info = mapped_data.get(visitor.id, {'page_count': 0, 'visitor_page_count': 0, 'page_ids': set()})
|
||||
visitor.page_ids = [(6, 0, visitor_info['page_ids'])]
|
||||
visitor.visitor_page_count = visitor_info['visitor_page_count']
|
||||
visitor.page_count = visitor_info['page_count']
|
||||
@@ -103,8 +109,8 @@ class WebsiteVisitor(models.Model):
|
||||
@api.depends('website_track_ids.page_id')
|
||||
def _compute_last_visited_page_id(self):
|
||||
results = self.env['website.track'].read_group([('visitor_id', 'in', self.ids)],
|
||||
['visitor_id', 'page_id', 'visit_datetime:max'],
|
||||
['visitor_id', 'page_id'], lazy=False)
|
||||
['visitor_id', 'page_id', 'visit_datetime:max'],
|
||||
['visitor_id', 'page_id'], lazy=False)
|
||||
mapped_data = {result['visitor_id'][0]: result['page_id'][0] for result in results if result['page_id']}
|
||||
for visitor in self:
|
||||
visitor.last_visited_page_id = mapped_data.get(visitor.id, False)
|
||||
@@ -153,23 +159,43 @@ class WebsiteVisitor(models.Model):
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_visitor_from_request(self, with_previous_visitors=False):
|
||||
def _get_visitor_from_request(self):
|
||||
""" Return the visitor as sudo from the request if there is a visitor_uuid cookie.
|
||||
It is possible that the partner has changed or has disconnected.
|
||||
In that case the cookie is still referencing the old visitor and need to be replaced
|
||||
with the one of the visitor returned !!!. """
|
||||
if not request:
|
||||
return None
|
||||
if with_previous_visitors and not request.env.user._is_public():
|
||||
Visitor = self.env['website.visitor'].sudo()
|
||||
visitor = Visitor
|
||||
access_token = request.httprequest.cookies.get('visitor_uuid')
|
||||
if access_token:
|
||||
visitor = Visitor.with_context(active_test=False).search([('access_token', '=', access_token)])
|
||||
|
||||
if not request.env.user._is_public():
|
||||
partner_id = request.env.user.partner_id
|
||||
# Retrieve all the previous partner's visitors to have full history of his last products viewed.
|
||||
return request.env['website.visitor'].sudo().with_context(active_test=False).search([('user_partner_id', '=', partner_id.id)])
|
||||
else:
|
||||
visitor = self.env['website.visitor']
|
||||
access_token = request.httprequest.cookies.get('visitor_id')
|
||||
if access_token:
|
||||
visitor = visitor.sudo().with_context(active_test=False).search([('access_token', '=', access_token)])
|
||||
return visitor
|
||||
if not visitor or visitor.user_partner_id != partner_id:
|
||||
# Partner and no cookie or wrong cookie
|
||||
visitor = Visitor.with_context(active_test=False).search([('user_partner_id', '=', partner_id.id)])
|
||||
elif visitor and visitor.user_partner_id:
|
||||
# Cookie associated to a Partner
|
||||
visitor = Visitor
|
||||
return visitor
|
||||
|
||||
def _get_visitor_from_request_or_create(self):
|
||||
""" Return a tuple (visitor, response), see _get_visitor_from_request
|
||||
If there is no visitor creates it and ensure the consistancy of the cookie. """
|
||||
visitor_sudo = self._get_visitor_from_request()
|
||||
if not visitor_sudo:
|
||||
visitor_sudo = self._create_visitor()
|
||||
return visitor_sudo
|
||||
|
||||
def _handle_webpage_dispatch(self, response, website_page):
|
||||
# get visitor. Done here to avoid having to do it multiple times in case of override.
|
||||
visitor_sudo = self._get_visitor_from_request()
|
||||
visitor_sudo = self._get_visitor_from_request_or_create()
|
||||
if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
|
||||
expiration_date = datetime.now() + timedelta(days=365)
|
||||
response.set_cookie('visitor_uuid', visitor_sudo.access_token, expires=expiration_date)
|
||||
self._handle_website_page_visit(response, website_page, visitor_sudo)
|
||||
|
||||
def _handle_website_page_visit(self, response, website_page, visitor_sudo):
|
||||
@@ -187,17 +213,12 @@ class WebsiteVisitor(models.Model):
|
||||
domain = [('page_id', '=', website_page.id)]
|
||||
else:
|
||||
domain = [('url', '=', url)]
|
||||
if not visitor_sudo:
|
||||
visitor_sudo = self._create_visitor(website_track_values)
|
||||
expiration_date = datetime.now() + timedelta(days=365)
|
||||
response.set_cookie('visitor_id', visitor_sudo.access_token, expires=expiration_date)
|
||||
else:
|
||||
visitor_sudo._add_tracking(domain, website_track_values)
|
||||
if visitor_sudo.lang_id.id != request.lang.id:
|
||||
visitor_sudo.write({'lang_id': request.lang.id})
|
||||
visitor_sudo._add_tracking(domain, website_track_values)
|
||||
if visitor_sudo.lang_id.id != request.lang.id:
|
||||
visitor_sudo.write({'lang_id': request.lang.id})
|
||||
|
||||
def _add_tracking(self, domain, website_track_values):
|
||||
""" Update the visitor when a website_track is added"""
|
||||
""" Add the track and update the visitor"""
|
||||
domain = expression.AND([domain, [('visitor_id', '=', self.id)]])
|
||||
last_view = self.env['website.track'].sudo().search(domain, limit=1)
|
||||
if not last_view or last_view.visit_datetime < datetime.now() - timedelta(minutes=30):
|
||||
|
||||
@@ -1214,38 +1214,40 @@ class WebsiteSale(http.Controller):
|
||||
# --------------------------------------------------------------------------
|
||||
@http.route('/shop/products/recently_viewed', type='json', auth='public', website=True)
|
||||
def products_recently_viewed(self, **kwargs):
|
||||
return self._get_products_recently_viewed()
|
||||
|
||||
def _get_products_recently_viewed(self):
|
||||
"""
|
||||
Returns list of recently viewed products according to current user and product options
|
||||
Returns list of recently viewed products according to current user
|
||||
"""
|
||||
max_number_of_product_for_carousel = 12
|
||||
visitors = request.env['website.visitor']._get_visitor_from_request(with_previous_visitors=True)
|
||||
if visitors:
|
||||
visitor = request.env['website.visitor']._get_visitor_from_request()
|
||||
if visitor:
|
||||
excluded_products = request.website.sale_get_order().mapped('order_line.product_id.id')
|
||||
products = request.env['website.track'].sudo().read_group(
|
||||
[('visitor_id', 'in', visitors.ids), ('product_id', '!=', False), ('product_id', 'not in', excluded_products)],
|
||||
[('visitor_id', '=', visitor.id), ('product_id', '!=', False), ('product_id', 'not in', excluded_products)],
|
||||
['product_id', 'visit_datetime:max'], ['product_id'], limit=max_number_of_product_for_carousel, orderby='visit_datetime DESC')
|
||||
products_ids = [product['product_id'][0] for product in products]
|
||||
if products_ids:
|
||||
viewed_products = request.env['product.product'].browse(products_ids)
|
||||
res = {
|
||||
'products': viewed_products.read(['id', 'name', 'website_url']),
|
||||
}
|
||||
|
||||
FieldMonetary = request.env['ir.qweb.field.monetary']
|
||||
monetary_options = {
|
||||
'display_currency': request.website.get_current_pricelist().currency_id,
|
||||
}
|
||||
rating = request.website.viewref('website_sale.product_comment').active
|
||||
for res_product, product in zip(res['products'], viewed_products):
|
||||
res = {'products': []}
|
||||
for product in viewed_products:
|
||||
combination_info = product._get_combination_info_variant()
|
||||
res_product = product.read(['id', 'name', 'website_url'])[0]
|
||||
res_product.update(combination_info)
|
||||
res_product['list_price'] = FieldMonetary.value_to_html(res_product['list_price'], monetary_options)
|
||||
res_product['price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options)
|
||||
if rating:
|
||||
res_product['rating'] = request.env["ir.ui.view"].render_template('website_rating.rating_widget_stars_static', values={
|
||||
'rating_avg': product.rating_avg,
|
||||
'rating_count': product.rating_count,
|
||||
})
|
||||
res['products'].append(res_product)
|
||||
|
||||
return res
|
||||
return {}
|
||||
@@ -1253,14 +1255,15 @@ class WebsiteSale(http.Controller):
|
||||
@http.route('/shop/products/recently_viewed_update', type='json', auth='public', website=True)
|
||||
def products_recently_viewed_update(self, product_id, **kwargs):
|
||||
res = {}
|
||||
Visitor = request.env['website.visitor']
|
||||
visitor = Visitor._get_visitor_from_request()
|
||||
if not visitor:
|
||||
visitor_sudo = Visitor._create_visitor({
|
||||
'product_id': product_id,
|
||||
'visit_datetime': datetime.now(),
|
||||
})
|
||||
res['visitor_id'] = visitor_sudo.access_token
|
||||
else:
|
||||
visitor._add_viewed_product(product_id)
|
||||
visitor_sudo = request.env['website.visitor']._get_visitor_from_request_or_create()
|
||||
if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
|
||||
res['visitor_uuid'] = visitor_sudo.access_token
|
||||
visitor_sudo._add_viewed_product(product_id)
|
||||
return res
|
||||
|
||||
@http.route('/shop/products/recently_viewed_delete', type='json', auth='public', website=True)
|
||||
def products_recently_viewed_delete(self, product_id, **kwargs):
|
||||
visitor_sudo = request.env['website.visitor']._get_visitor_from_request()
|
||||
if visitor_sudo:
|
||||
request.env['website.track'].sudo().search([('visitor_id', '=', visitor_sudo.id), ('product_id', '=', product_id)]).unlink()
|
||||
return self._get_products_recently_viewed()
|
||||
|
||||
@@ -14,6 +14,7 @@ publicWidget.registry.productsRecentlyViewedSnippet = publicWidget.Widget.extend
|
||||
xmlDependencies: ['/website_sale/static/src/xml/website_sale_recently_viewed.xml'],
|
||||
events: {
|
||||
'click .js_add_cart': '_onAddToCart',
|
||||
'click .js_remove': '_onRemove',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -118,6 +119,24 @@ publicWidget.registry.productsRecentlyViewedSnippet = publicWidget.Widget.extend
|
||||
self._dp.add(self._fetch()).then(self._render.bind(self));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove product from recently viewed products.
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onRemove: function (ev) {
|
||||
var self = this;
|
||||
var $card = $(ev.currentTarget).closest('.card');
|
||||
this._rpc({
|
||||
route: "/shop/products/recently_viewed_delete",
|
||||
params: {
|
||||
product_id: $card.find('input[data-product-id]').data('product-id'),
|
||||
},
|
||||
}).then(function (data) {
|
||||
self._render(data);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.productsRecentlyViewedUpdate = publicWidget.Widget.extend({
|
||||
@@ -162,8 +181,8 @@ publicWidget.registry.productsRecentlyViewedUpdate = publicWidget.Widget.extend(
|
||||
product_id: productId,
|
||||
}
|
||||
}).then(function (res) {
|
||||
if (res && res.visitor_id) {
|
||||
utils.set_cookie('visitor_id', res.visitor_id);
|
||||
if (res && res.visitor_uuid) {
|
||||
utils.set_cookie('visitor_uuid', res.visitor_uuid);
|
||||
}
|
||||
utils.set_cookie(cookieName, productId, 30 * 60);
|
||||
});
|
||||
|
||||
@@ -553,6 +553,9 @@ a.no-decoration {
|
||||
height: 12rem;
|
||||
}
|
||||
}
|
||||
.o_carousel_product_img_link:hover + .o_carousel_product_remove {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.o_carousel_product_card_wrap {
|
||||
@@ -568,3 +571,15 @@ a.no-decoration {
|
||||
border-radius: 5px;
|
||||
background-color: $o-enterprise-primary-color;
|
||||
}
|
||||
|
||||
.o_carousel_product_remove {
|
||||
position: absolute;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
right: 5%;
|
||||
top: 5%;
|
||||
}
|
||||
|
||||
.o_carousel_product_remove:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
<div t-attf-class="o_carousel_product_card_wrap col-md-#{12 / productFrame}">
|
||||
<div class="o_carousel_product_card card h-100">
|
||||
<input type="hidden" name="product-id" t-att-data-product-id="product.id"/>
|
||||
<a t-att-href="product.website_url">
|
||||
<a class="o_carousel_product_img_link" t-att-href="product.website_url">
|
||||
<img class="o_carousel_product_card_img_top card-img-top" t-attf-src="/web/image/product.product/#{product.id}#{productFrame == 1 ? '/image_256' : '/image_512'}" t-att-alt="product.display_name"/>
|
||||
</a>
|
||||
<i class="fa fa-trash o_carousel_product_remove js_remove"></i>
|
||||
<div class="o_carousel_product_card_body card-body border-top">
|
||||
<a t-att-href="product.website_url" class="text-decoration-none">
|
||||
<h6 class="card-title mb-0 text-truncate" t-raw="product.display_name"/>
|
||||
|
||||
Reference in New Issue
Block a user