From 7d6479e1751adb8513a36f0201509099cfae2fd5 Mon Sep 17 00:00:00 2001 From: fja-odoo Date: Wed, 18 Sep 2019 17:35:41 +0000 Subject: [PATCH] [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 --- addons/website/models/res_users.py | 17 +++-- addons/website/models/website_visitor.py | 69 ++++++++++++------- addons/website_sale/controllers/main.py | 41 ++++++----- .../src/js/website_sale_recently_viewed.js | 23 ++++++- .../static/src/scss/website_sale.scss | 15 ++++ .../src/xml/website_sale_recently_viewed.xml | 3 +- 6 files changed, 117 insertions(+), 51 deletions(-) diff --git a/addons/website/models/res_users.py b/addons/website/models/res_users.py index c652604cae6..790ed531635 100644 --- a/addons/website/models/res_users.py +++ b/addons/website/models/res_users.py @@ -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 diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index effcdc718fd..d17317a6340 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -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): diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index a311c3f1f3f..c231cdbb4ed 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -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() diff --git a/addons/website_sale/static/src/js/website_sale_recently_viewed.js b/addons/website_sale/static/src/js/website_sale_recently_viewed.js index e587aab01b9..f0c0b9353c2 100644 --- a/addons/website_sale/static/src/js/website_sale_recently_viewed.js +++ b/addons/website_sale/static/src/js/website_sale_recently_viewed.js @@ -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); }); diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index 4ce1008a10a..cb9b44b8ed4 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -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; +} diff --git a/addons/website_sale/static/src/xml/website_sale_recently_viewed.xml b/addons/website_sale/static/src/xml/website_sale_recently_viewed.xml index d13bff2c49d..34b9ec0ea5f 100644 --- a/addons/website_sale/static/src/xml/website_sale_recently_viewed.xml +++ b/addons/website_sale/static/src/xml/website_sale_recently_viewed.xml @@ -11,9 +11,10 @@