[IMP] website, *: use upsert to improve visitor perf
* im_livechat, test_event_full, website_blog, website_crm,
website_event, website_event_track, website_event_track_quiz,
webite_livechat, website_sale
There is 6 main changes in this commit:
1. Using raw SQL Upsert instead of the ORM methods. While raw SQL should
generally be avoided, it makes sense for such a low level behavior which
is impacting every flows.
Indeed, tracking visitors is a generic behavior done on all pages and
controllers. It is important to optimize it to reduce processing time
and SQL Queries.
Benchmark of that change alone:
> Rendering a tracked page improves from ~19.5ms to ~17ms (using `ab`
with 1000 loop) and the requests involved in the tracking process are
reduced from 8 SQL Queries to 3:
- 1 request to upsert the visitor
- 1 request to fetch the visitor data
- 1 request to add the tracking record
2. Adding in that upsert query the `visitor.track` insert, creating both
records in one go, bringing the query count from 3 to 2.
3. Refactoring of the `parent_id` behavior that was introduced in stable
with [1]. The purpose was to keep track of multiple visitor linked to a
same user to merge the tracking together. Especially useful for tracking
a same visitor on different devices (when logged in).
Only one visitor was kept as active, others would be archived and their
tracks would be set/moved to the main partner.
Removing those duplicate visitor was not possible because those archived
duplicated visitor were holding the devices notification push token.
Since [2], those token were moved to their own table, all related to the
main visitor.
We can then now safely remove those duplicate visitors after merging
their track to the main visitor. Thus, the `parent_id` field is no more
useful. Removing it removes a layer of complexity.
Note that thanks to this part, the `active` field can also be removed.
4. Deeper functionnal change, inspired from Plausible: The access_token
is no more stored in a cookie but is the result of a hashing method
based on <IP Adress, User Agent>.
The reason behind that change is that, in an upcoming refactoring,
sessions won't be stored anymore unless absolutely needed (login, add to
cart..). It will also ship a no cookies policy, trying to get rid of all
cookies.
This change is bringing some functional changes:
- Since the IP is included in the hash to generate the token, it means
that:
A. If an anonymous user switch IP (eg from 4G to wifi), it is
considered as a new visitor.
B. If 2 anonymous users with the exact same user agent (same browser,
same browser version, same exact os or phone) are on the same IP,
those will be considered as the same visitor.
- Since the request host is not included in the hash, it means that
visiting a DB from 2 differents URLs (domain and/or ip) on the same
device and same browser will result in a shared visitor.
It shouldn't imply any issue as this is A. not wrong and B. mostly
used for tests.
As all this is only related to non logged in user, it shouldn't be a
real issue as anonymous visitors are not supposed to be meant to be
business critical, even if we use them for "a bit more" than simple
analytics data.
5. The access_token is now replaced by the partner_id once the user logs
in, so:
- We don't need to either search on the partner_id field or the
access_token field (depending if the user is logged in or not), we can
only use the access_token row/field to do both.
- On logout, everything works out of the box as the access_token will be
regenerated since there is no partner_id anymore.
- On login, if an access_token matches the user's partner_id, that
visitor is returned.
If there is no such token, a new visitor is created for that partner_id.
In both 2 cases, tracks are moved to that visitor and the anonymous
visitor is removed.
- We can remove the code that was in charge of checking if the
access_token / visitor cookie was wrong (coming from another user eg,
different user login on same device). Indeed, such collision is not
possible anymore as the access_token automatically match the logged in
user.
- We can remove the code that was in charge of checking if the
access_token / visitor cookie was wrong (coming from a logged in user
while the current visitor is not loggedin). Such collision is not
possible anymore as the access_token is (re)generated as an anonymous
token (hash) when not logged in.
6. There is no more check to prevent a track to be created if there was
already a track for that URL in the last 30 minutes.
While this can easily be re-introduced (one CTE on the upsert), it was
adding ~100ms (from ~20 to ~110ms) to the request on a big database as
Odoo where there is ~100 millions tracks and ~100 millions visitors.
It has been validated that it was not a real issue as it is not
fundamentally wrong. If a visitor visited 20 times a product or a
specific page in that short amount of time, you might want to know that
because the user is most likely interested by it.
Changes (1+2), 3, (4+5) and 6 are all independant from each other and
could have existed on their own.
[1]: https://github.com/odoo/odoo/commit/c6b8a44b970a46dcd87a4e2cb1ad52fa340b209f
[2]: https://github.com/odoo/enterprise/pull/16781/commits/f75090fe8b42484e89e933976e8441d2f5eb9415
task-2867045
closes odoo/odoo#87857
Related: odoo/enterprise#28004
Related: odoo/upgrade#3566
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
This commit is contained in:
@@ -452,7 +452,7 @@ class TestOnlineEventPerformance(EventPerformanceCase, UtilPerf):
|
||||
# website customer data
|
||||
with freeze_time(self.reference_now):
|
||||
self.authenticate(None, None)
|
||||
with self.assertQueryCount(default=39): # com runbot: 39 - ent runbot: 39
|
||||
with self.assertQueryCount(default=34): # com runbot: 34 - ent runbot: 34
|
||||
self._test_url_open('/event')
|
||||
|
||||
# @warmup
|
||||
|
||||
@@ -37,10 +37,8 @@ class TestWEventRegister(TestWEventCommon):
|
||||
)
|
||||
|
||||
# check visitor stored information
|
||||
self.assertEqual(visitor.name, "Raoulette Poiluchette")
|
||||
self.assertEqual(visitor.display_name, "Raoulette Poiluchette")
|
||||
self.assertEqual(visitor.event_registration_ids, new_registrations)
|
||||
self.assertEqual(visitor.partner_id, self.env['res.partner'])
|
||||
self.assertEqual(visitor.mobile, "0456112233")
|
||||
self.assertEqual(visitor.email, "raoulette@example.com")
|
||||
self.assertFalse(visitor.parent_id)
|
||||
self.assertTrue(visitor.active)
|
||||
|
||||
@@ -2,34 +2,22 @@
|
||||
<odoo>
|
||||
|
||||
<record id="website_visitor_0" model="website.visitor">
|
||||
<field name="name">Edwin Hansen</field>
|
||||
<field name="partner_id" ref="base.res_partner_address_5"/>
|
||||
<field name="country_id" ref="base.nl"/>
|
||||
<field name="page_ids" eval="[(4, ref('website.homepage_page'))]"/>
|
||||
<field name="website_track_ids" eval="[(0, 0, {'page_id': ref('website.homepage_page'), 'url': '/'})]"/>
|
||||
<field name="access_token" ref="base.res_partner_address_5"/>
|
||||
</record>
|
||||
<record id="website_visitor_1" model="website.visitor">
|
||||
<field name="name">Soham Palmer</field>
|
||||
<field name="partner_id" ref="base.res_partner_address_11"/>
|
||||
<field name="country_id" ref="base.us"/>
|
||||
<field name="page_ids" eval="[(4, ref('website.contactus_page'))]"/>
|
||||
<field name="website_track_ids" eval="[(0, 0, {'page_id': ref('website.contactus_page'), 'url': '/contactus'})]"/>
|
||||
<field name="access_token" ref="base.res_partner_address_11"/>
|
||||
</record>
|
||||
<record id="website_visitor_2" model="website.visitor">
|
||||
<field name="name">Philipe J. Fry</field>
|
||||
<field name="partner_id" eval="False"/>
|
||||
<field name="country_id" ref="base.be"/>
|
||||
<field name="page_ids" eval="[(4, ref('website.homepage_page'))]"/>
|
||||
<field name="website_track_ids" eval="[(0, 0, {'page_id': ref('website.homepage_page'), 'url': '/'})]"/>
|
||||
</record>
|
||||
<record id="website_visitor_2_1" model="website.visitor">
|
||||
<field name="name">Philipe J. Fry (old)</field>
|
||||
<field name="active" eval="False"></field>
|
||||
<field name="parent_id" ref="website.website_visitor_2"/>
|
||||
<field name="partner_id" eval="False"/>
|
||||
<field name="country_id" ref="base.be"/>
|
||||
<field name="page_ids" eval="[(4, ref('website.contactus_page'))]"/>
|
||||
<field name="website_track_ids" eval="[(0, 0, {'page_id': ref('website.contactus_page'), 'url': '/contactus'})]"/>
|
||||
<field name="access_token">f9d24e459528093a790f38e5cc989a8d</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -148,7 +148,7 @@ class Http(models.AbstractModel):
|
||||
|
||||
view = template and request.env['website'].get_template(template)
|
||||
if view and view.track:
|
||||
request.env['website.visitor']._handle_webpage_dispatch(response, website_page)
|
||||
request.env['website.visitor']._handle_webpage_dispatch(website_page)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import request
|
||||
|
||||
@@ -69,32 +69,27 @@ class ResUsers(models.Model):
|
||||
@classmethod
|
||||
def authenticate(cls, db, login, password, user_agent_env):
|
||||
""" Override to link the logged in user's res.partner to website.visitor.
|
||||
If both a request-based visitor and a user-based visitor exist we try
|
||||
to update them (have same partner_id), and move sub records to the main
|
||||
visitor (user one). Purpose is to try to keep a main visitor with as
|
||||
much sub-records (tracked pages, leads, ...) as possible. """
|
||||
If a visitor already exists for that user, assign it data from the
|
||||
current anonymous visitor (if exists).
|
||||
Purpose is to try to aggregate as much sub-records (tracked pages,
|
||||
leads, ...) as possible. """
|
||||
visitor_pre_authenticate_sudo = None
|
||||
if request and request.env:
|
||||
visitor_pre_authenticate_sudo = request.env['website.visitor']._get_visitor_from_request()
|
||||
uid = super(ResUsers, cls).authenticate(db, login, password, user_agent_env)
|
||||
if uid:
|
||||
if uid and visitor_pre_authenticate_sudo:
|
||||
with cls.pool.cursor() as cr:
|
||||
env = api.Environment(cr, uid, {})
|
||||
visitor_sudo = env['website.visitor']._get_visitor_from_request()
|
||||
if visitor_sudo:
|
||||
user_partner = env.user.partner_id
|
||||
other_user_visitor_sudo = env['website.visitor'].with_context(active_test=False).sudo().search(
|
||||
[('partner_id', '=', user_partner.id), ('id', '!=', visitor_sudo.id)],
|
||||
order='last_connection_datetime DESC',
|
||||
) # current 13.3 state: 1 result max as unique visitor / partner
|
||||
if other_user_visitor_sudo:
|
||||
visitor_main = other_user_visitor_sudo[0]
|
||||
other_visitors = other_user_visitor_sudo[1:] # normally void
|
||||
(visitor_sudo + other_visitors)._link_to_visitor(visitor_main)
|
||||
visitor_main.name = user_partner.name
|
||||
visitor_main.active = True
|
||||
visitor_main._update_visitor_last_visit()
|
||||
else:
|
||||
if visitor_sudo.partner_id != user_partner:
|
||||
visitor_sudo._link_to_partner(
|
||||
user_partner,
|
||||
update_values={'partner_id': user_partner.id})
|
||||
visitor_sudo._update_visitor_last_visit()
|
||||
user_partner = env.user.partner_id
|
||||
visitor_current_user_sudo = env['website.visitor'].sudo().search([
|
||||
('partner_id', '=', user_partner.id)
|
||||
], limit=1)
|
||||
if visitor_current_user_sudo:
|
||||
# A visitor exists for the logged in user, link public
|
||||
# visitor records to it.
|
||||
visitor_pre_authenticate_sudo._merge_visitor(visitor_current_user_sudo)
|
||||
visitor_current_user_sudo._update_visitor_last_visit()
|
||||
else:
|
||||
visitor_pre_authenticate_sudo.access_token = user_partner.id
|
||||
visitor_pre_authenticate_sudo._update_visitor_last_visit()
|
||||
return uid
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
from psycopg2 import sql
|
||||
|
||||
import hashlib
|
||||
import pytz
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
@@ -28,14 +30,29 @@ class WebsiteTrack(models.Model):
|
||||
class WebsiteVisitor(models.Model):
|
||||
_name = 'website.visitor'
|
||||
_description = 'Website Visitor'
|
||||
_order = 'last_connection_datetime DESC'
|
||||
_order = 'id DESC'
|
||||
|
||||
name = fields.Char('Name')
|
||||
access_token = fields.Char(required=True, default=lambda x: uuid.uuid4().hex, copy=False, groups='website.group_website_publisher')
|
||||
active = fields.Boolean('Active', default=True)
|
||||
def _get_access_token(self):
|
||||
""" Either the user's partner.id or a hash. """
|
||||
if not request:
|
||||
raise ValueError("Visitors can only be created through the frontend.")
|
||||
|
||||
if not self.env.user._is_public():
|
||||
return self.env.user.partner_id.id
|
||||
|
||||
msg = repr((
|
||||
request.httprequest.remote_addr,
|
||||
request.httprequest.environ.get('HTTP_USER_AGENT'),
|
||||
request.session.sid,
|
||||
)).encode('utf-8')
|
||||
# Keep same length (32) as before, it will ease the migration without
|
||||
# any real downside
|
||||
return hashlib.sha1(msg).hexdigest()[:32]
|
||||
|
||||
name = fields.Char('Name', related='partner_id.name')
|
||||
access_token = fields.Char(required=True, default=_get_access_token, copy=False)
|
||||
website_id = fields.Many2one('website', "Website", readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', string="Contact", help="Partner of the last logged in user.", index='btree_not_null')
|
||||
parent_id = fields.Many2one('website.visitor', string="Parent", ondelete='set null', index='btree_not_null', help="Main identity")
|
||||
partner_id = fields.Many2one('res.partner', string="Contact", help="Partner of the last logged in user.", compute='_compute_partner_id', store=True, index='btree_not_null')
|
||||
partner_image = fields.Binary(related='partner_id.image_1920')
|
||||
|
||||
# localisation and info
|
||||
@@ -62,19 +79,27 @@ class WebsiteVisitor(models.Model):
|
||||
|
||||
_sql_constraints = [
|
||||
('access_token_unique', 'unique(access_token)', 'Access token should be unique.'),
|
||||
('partner_uniq', 'unique(partner_id)', 'A partner is linked to only one visitor.'),
|
||||
]
|
||||
|
||||
@api.depends('name')
|
||||
@api.depends('partner_id')
|
||||
def name_get(self):
|
||||
res = []
|
||||
for record in self:
|
||||
res.append((
|
||||
record.id,
|
||||
record.name or _('Website Visitor #%s', record.id)
|
||||
record.partner_id.name or _('Website Visitor #%s', record.id)
|
||||
))
|
||||
return res
|
||||
|
||||
@api.depends('access_token')
|
||||
def _compute_partner_id(self):
|
||||
# The browse in the loop is fine, there is no SQL Query on partner here
|
||||
for visitor in self:
|
||||
# If the access_token is not a 32 length hexa string, it means that
|
||||
# the visitor is linked to a logged in user, in which case its
|
||||
# partner_id is used instead as the token.
|
||||
visitor.partner_id = len(visitor.access_token) != 32 and self.env['res.partner'].browse([visitor.access_token])
|
||||
|
||||
@api.depends('partner_id.email_normalized', 'partner_id.mobile', 'partner_id.phone')
|
||||
def _compute_email_phone(self):
|
||||
results = self.env['res.partner'].search_read(
|
||||
@@ -118,9 +143,10 @@ 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)
|
||||
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)
|
||||
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)
|
||||
@@ -166,88 +192,115 @@ class WebsiteVisitor(models.Model):
|
||||
'context': compose_ctx,
|
||||
}
|
||||
|
||||
def _get_visitor_from_request(self, force_create=False):
|
||||
""" Return the visitor as sudo from the request if there is a
|
||||
visitor_uuid cookie.
|
||||
def _upsert_visitor(self, access_token, force_track_values=None):
|
||||
""" Based on the given `access_token`, either create or return the
|
||||
related visitor if exists, through a single raw SQL UPSERT Query.
|
||||
|
||||
When fetching visitor, now that duplicates are linked to a main visitor
|
||||
instead of unlinked, you may have more collisions issues with cookie
|
||||
being set (after a de-connection for example).
|
||||
It will also create a tracking record if requested, in the same query.
|
||||
|
||||
The visitor associated to a partner in case of public user is not taken
|
||||
into account, it is considered as desynchronized cookie.
|
||||
In addition, we also discard if the visitor has a main visitor whose
|
||||
partner is set (aka wrong after logout partner). """
|
||||
:param access_token: token to be used to upsert the visitor
|
||||
:param force_track_values: an optional dict to create a track at the
|
||||
same time.
|
||||
:return: a tuple containing the visitor id and the upsert result (either
|
||||
`inserted` or `updated).
|
||||
"""
|
||||
country_code = request.geoip.get('country_code')
|
||||
country_id = request.env['res.country'].sudo().search([
|
||||
('code', '=', country_code)
|
||||
], limit=1).id if country_code else None
|
||||
create_values = {
|
||||
'access_token': access_token,
|
||||
'lang_id': request.lang.id,
|
||||
'country_id': country_id,
|
||||
'website_id': request.website.id,
|
||||
'timezone': self._get_visitor_timezone() or None,
|
||||
'write_uid': self.env.uid,
|
||||
'create_uid': self.env.uid,
|
||||
# If the access_token is not a 32 length hexa string, it means that the
|
||||
# visitor is linked to a logged in user, in which case its partner_id is
|
||||
# used instead as the token.
|
||||
'partner_id': None if len(str(access_token)) == 32 else access_token,
|
||||
}
|
||||
|
||||
query = """
|
||||
INSERT INTO website_visitor (
|
||||
partner_id, access_token, last_connection_datetime, visit_count, lang_id,
|
||||
country_id, website_id, timezone, write_uid, create_uid, write_date, create_date)
|
||||
VALUES (
|
||||
%(partner_id)s, %(access_token)s, now() at time zone 'UTC', 1, %(lang_id)s,
|
||||
%(country_id)s, %(website_id)s, %(timezone)s, %(create_uid)s, %(write_uid)s,
|
||||
now() at time zone 'UTC', now() at time zone 'UTC')
|
||||
ON CONFLICT (access_token)
|
||||
DO UPDATE SET
|
||||
last_connection_datetime=excluded.last_connection_datetime,
|
||||
visit_count = CASE WHEN website_visitor.last_connection_datetime < NOW() AT TIME ZONE 'UTC' - INTERVAL '8 hours'
|
||||
THEN website_visitor.visit_count + 1
|
||||
ELSE website_visitor.visit_count
|
||||
END
|
||||
RETURNING id, CASE WHEN create_date = now() at time zone 'UTC' THEN 'inserted' ELSE 'updated' END AS upsert
|
||||
"""
|
||||
|
||||
if force_track_values:
|
||||
create_values['url'] = force_track_values['url']
|
||||
create_values['page_id'] = force_track_values.get('page_id')
|
||||
query = sql.SQL("""
|
||||
WITH visitor AS (
|
||||
{query}, %(url)s AS url, %(page_id)s AS page_id
|
||||
), track AS (
|
||||
INSERT INTO website_track (visitor_id, url, page_id, visit_datetime)
|
||||
SELECT id, url, page_id::integer, now() at time zone 'UTC' FROM visitor
|
||||
)
|
||||
SELECT id, upsert from visitor;
|
||||
""").format(query=sql.SQL(query))
|
||||
|
||||
self.env.cr.execute(query, create_values)
|
||||
return self.env.cr.fetchone()
|
||||
|
||||
def _get_visitor_from_request(self, force_create=False, force_track_values=None):
|
||||
""" Return the visitor as sudo from the request.
|
||||
|
||||
:param bool force_create: force a visitor creation if no visitor exists
|
||||
:param force_track_values: an optional dict to create a track at the
|
||||
same time.
|
||||
:return: the website visitor if exists or forced, empty recordset
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
# This function can be called in json with mobile app.
|
||||
# In case of mobile app, no uid is set on the jsonRequest env.
|
||||
# In case of multi db, _env is None on request, and request.env unbound.
|
||||
if not (request and request.env and request.env.uid):
|
||||
return None
|
||||
|
||||
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)])
|
||||
# Prefetch access_token and other fields. Since access_token has a restricted group and we access
|
||||
# a non restricted field (partner_id) first it is not fetched and will require an additional query to be retrieved.
|
||||
visitor.access_token
|
||||
access_token = self._get_access_token()
|
||||
|
||||
if not self.env.user._is_public():
|
||||
partner_id = self.env.user.partner_id
|
||||
if not visitor or visitor.partner_id and visitor.partner_id != partner_id:
|
||||
# Partner and no cookie or wrong cookie
|
||||
visitor = Visitor.with_context(active_test=False).search([('partner_id', '=', partner_id.id)])
|
||||
elif visitor and visitor.partner_id:
|
||||
# Cookie associated to a Partner
|
||||
visitor = Visitor
|
||||
if force_create:
|
||||
visitor_id, _ = self._upsert_visitor(access_token, force_track_values)
|
||||
visitor = Visitor.browse(visitor_id)
|
||||
else:
|
||||
visitor = Visitor.search([('access_token', '=', access_token)])
|
||||
|
||||
# also check that visitor parent partner is not different from user's one
|
||||
# (indicates duplicate due to invalid or wrong cookie)
|
||||
if visitor and visitor.parent_id.partner_id:
|
||||
if self.env.user._is_public():
|
||||
visitor = self.env['website.visitor'].sudo()
|
||||
elif not visitor.partner_id:
|
||||
visitor = self.env['website.visitor'].sudo().with_context(active_test=False).search(
|
||||
[('partner_id', '=', self.env.user.partner_id.id)]
|
||||
)
|
||||
|
||||
if visitor and not visitor.timezone:
|
||||
if not force_create and visitor and not visitor.timezone:
|
||||
tz = self._get_visitor_timezone()
|
||||
if tz:
|
||||
visitor._update_visitor_timezone(tz)
|
||||
|
||||
if not visitor and force_create:
|
||||
visitor = self._create_visitor()
|
||||
|
||||
return visitor
|
||||
|
||||
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(force_create=True)
|
||||
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(website_page, visitor_sudo)
|
||||
def _handle_webpage_dispatch(self, website_page):
|
||||
""" Create a website.visitor if the http request object is a tracked
|
||||
website.page or a tracked ir.ui.view.
|
||||
Since this method is only called on tracked elements, the
|
||||
last_connection_datetime might not be accurate as the visitor could have
|
||||
been visiting only untracked page during his last visit."""
|
||||
|
||||
def _handle_website_page_visit(self, website_page, visitor_sudo):
|
||||
""" Called on dispatch. This will create a website.visitor if the http request object
|
||||
is a tracked website page or a tracked view. Only on tracked elements to avoid having
|
||||
too much operations done on every page or other http requests.
|
||||
Note: The side effect is that the last_connection_datetime is updated ONLY on tracked elements."""
|
||||
url = request.httprequest.url
|
||||
website_track_values = {
|
||||
'url': url,
|
||||
'visit_datetime': datetime.now(),
|
||||
}
|
||||
website_track_values = {'url': url}
|
||||
if website_page:
|
||||
website_track_values['page_id'] = website_page.id
|
||||
domain = [('page_id', '=', website_page.id)]
|
||||
else:
|
||||
domain = [('url', '=', url)]
|
||||
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})
|
||||
self._get_visitor_from_request(force_create=True, force_track_values=website_track_values)
|
||||
|
||||
def _add_tracking(self, domain, website_track_values):
|
||||
""" Add the track and update the visitor"""
|
||||
@@ -258,76 +311,44 @@ class WebsiteVisitor(models.Model):
|
||||
self.env['website.track'].create(website_track_values)
|
||||
self._update_visitor_last_visit()
|
||||
|
||||
def _create_visitor(self):
|
||||
""" Create a visitor. Tracking is added after the visitor has been created."""
|
||||
country_code = request.geoip.get('country_code')
|
||||
country_id = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1).id if country_code else False
|
||||
vals = {
|
||||
'lang_id': request.lang.id,
|
||||
'country_id': country_id,
|
||||
'website_id': request.website.id,
|
||||
}
|
||||
def _merge_visitor(self, target):
|
||||
""" Merge an anonymous visitor data to a partner visitor then unlink
|
||||
that anonymous visitor.
|
||||
Purpose is to try to aggregate as much sub-records (tracked pages,
|
||||
leads, ...) as possible.
|
||||
It is especially useful to aggregate data from the same user on
|
||||
different devices.
|
||||
|
||||
tz = self._get_visitor_timezone()
|
||||
if tz:
|
||||
vals['timezone'] = tz
|
||||
This method is meant to be overridden for other modules to merge their
|
||||
own anonymous visitor data to the partner visitor before unlink.
|
||||
|
||||
if not self.env.user._is_public():
|
||||
vals['partner_id'] = self.env.user.partner_id.id
|
||||
vals['name'] = self.env.user.partner_id.name
|
||||
return self.sudo().create(vals)
|
||||
|
||||
def _link_to_partner(self, partner, update_values=None):
|
||||
""" Link visitors to a partner. This method is meant to be overridden in
|
||||
order to propagate, if necessary, partner information to sub records.
|
||||
|
||||
:param partner: partner used to link sub records;
|
||||
:param update_values: optional values to update visitors to link;
|
||||
"""
|
||||
vals = {'name': partner.name}
|
||||
if update_values:
|
||||
vals.update(update_values)
|
||||
self.write(vals)
|
||||
|
||||
def _link_to_visitor(self, target):
|
||||
""" Link visitors to target visitors, because they are linked to the
|
||||
same identity. Purpose is mainly to propagate partner identity to sub
|
||||
records to ease database update and decide what to do with "duplicated".
|
||||
This method is meant to be overridden in order to implement some specific
|
||||
behavior linked to sub records of duplicate management.
|
||||
This method is only called after the user logs in.
|
||||
|
||||
:param target: main visitor, target of link process;
|
||||
"""
|
||||
# Link sub records of self to target partner
|
||||
if target.partner_id:
|
||||
self._link_to_partner(target.partner_id)
|
||||
# Link sub records of self to target visitor
|
||||
self.website_track_ids.write({'visitor_id': target.id})
|
||||
|
||||
# Archive current record and set its parent visitor
|
||||
self.partner_id = False
|
||||
self.parent_id = target.id
|
||||
self.active = False
|
||||
|
||||
return target
|
||||
if not target.partner_id:
|
||||
raise ValueError("The `target` visitor should be linked to a partner.")
|
||||
self.website_track_ids.visitor_id = target.id
|
||||
self.unlink()
|
||||
|
||||
def _cron_unlink_old_visitors(self):
|
||||
""" Unlink inactive visitors (see '_inactive_visitors_domain' for details).
|
||||
""" Unlink inactive visitors (see '_inactive_visitors_domain' for
|
||||
details).
|
||||
|
||||
Visitors were previously archived but we came to the conclusion that archived visitors
|
||||
have very little value and bloat the database for no reason. """
|
||||
Visitors were previously archived but we came to the conclusion that
|
||||
archived visitors have very little value and bloat the database for no
|
||||
reason. """
|
||||
|
||||
inactive_visitors = self.env['website.visitor'].sudo() \
|
||||
.with_context(active_test=False) \
|
||||
.search(self._inactive_visitors_domain())
|
||||
inactive_visitors.unlink()
|
||||
self.env['website.visitor'].sudo().search(self._inactive_visitors_domain()).unlink()
|
||||
|
||||
def _inactive_visitors_domain(self):
|
||||
""" This method defines the domain of visitors that can be cleaned. By default visitors
|
||||
not linked to any partner and not active for 'website.visitor.live.days' days (default being 60)
|
||||
are considered as inactive.
|
||||
""" This method defines the domain of visitors that can be cleaned. By
|
||||
default visitors not linked to any partner and not active for
|
||||
'website.visitor.live.days' days (default being 60) are considered as
|
||||
inactive.
|
||||
|
||||
This method is meant to be overridden by sub-modules to further refine inactivity conditions. """
|
||||
This method is meant to be overridden by sub-modules to further refine
|
||||
inactivity conditions. """
|
||||
|
||||
delay_days = int(self.env['ir.config_parameter'].sudo().get_param('website.visitor.live.days', 60))
|
||||
deadline = datetime.now() - timedelta(days=delay_days)
|
||||
@@ -346,24 +367,18 @@ class WebsiteVisitor(models.Model):
|
||||
self.env.cr.execute(query, (timezone, self.id))
|
||||
|
||||
def _update_visitor_last_visit(self):
|
||||
""" We need to do this part here to avoid concurrent updates error. """
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
query_lock = "SELECT * FROM website_visitor where id = %s FOR NO KEY UPDATE NOWAIT"
|
||||
self.env.cr.execute(query_lock, (self.id,), log_exceptions=False)
|
||||
|
||||
date_now = datetime.now()
|
||||
query = "UPDATE website_visitor SET "
|
||||
if self.last_connection_datetime < (date_now - timedelta(hours=8)):
|
||||
query += "visit_count = visit_count + 1,"
|
||||
query += """
|
||||
active = True,
|
||||
last_connection_datetime = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
self.env.cr.execute(query, (date_now, self.id), log_exceptions=False)
|
||||
except Exception:
|
||||
pass
|
||||
date_now = datetime.now()
|
||||
query = "UPDATE website_visitor SET "
|
||||
if self.last_connection_datetime < (date_now - timedelta(hours=8)):
|
||||
query += "visit_count = visit_count + 1,"
|
||||
query += """
|
||||
last_connection_datetime = %s
|
||||
WHERE id IN (
|
||||
SELECT id FROM website_visitor WHERE id = %s
|
||||
FOR NO KEY UPDATE SKIP LOCKED
|
||||
)
|
||||
"""
|
||||
self.env.cr.execute(query, (date_now, self.id), log_exceptions=False)
|
||||
|
||||
def _get_visitor_timezone(self):
|
||||
tz = request.httprequest.cookies.get('tz') if request else None
|
||||
|
||||
@@ -104,16 +104,16 @@ class TestWebsitePerformance(UtilPerf):
|
||||
def test_15_perf_sql_queries_page(self):
|
||||
# standard tracked website.page
|
||||
self.page.track = True
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url), 14)
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url, cache=False), 18)
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url), 7)
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url, cache=False), 11)
|
||||
self.menu.unlink()
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url), 14)
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url, cache=False), 18)
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url), 7)
|
||||
self.assertEqual(self._get_url_hot_query(self.page.url, cache=False), 11)
|
||||
|
||||
def test_20_perf_sql_queries_homepage(self):
|
||||
# homepage "/" has its own controller
|
||||
self.assertEqual(self._get_url_hot_query('/'), 14)
|
||||
self.assertEqual(self._get_url_hot_query('/', cache=False), 16)
|
||||
self.assertEqual(self._get_url_hot_query('/'), 7)
|
||||
self.assertEqual(self._get_url_hot_query('/', cache=False), 9)
|
||||
|
||||
def test_30_perf_sql_queries_page_no_layout(self):
|
||||
# website.page with no call to layout templates
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# coding: utf-8
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import random
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
from odoo.addons.website.models.website_visitor import WebsiteVisitor
|
||||
from odoo.tests import common, tagged
|
||||
|
||||
@@ -34,7 +35,6 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
self.website = self.env['website'].search([
|
||||
('company_id', '=', self.env.user.company_id.id)
|
||||
], limit=1)
|
||||
self.cookies = {}
|
||||
|
||||
untracked_view = self.env['ir.ui.view'].create({
|
||||
'name': 'UntackedView',
|
||||
@@ -122,17 +122,28 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
def assertVisitorDeactivated(self, visitor, main_visitor):
|
||||
""" Method that checks that a visitor has been de-activated / merged
|
||||
with other visitor, notably in case of login (see User.authenticate() as
|
||||
well as Visitor._link_to_visitor() ). """
|
||||
self.assertTrue(bool(visitor))
|
||||
self.assertFalse(visitor.active)
|
||||
self.assertTrue(main_visitor.active)
|
||||
self.assertEqual(visitor.parent_id, main_visitor)
|
||||
|
||||
# tracks are linked
|
||||
well as Visitor._merge_visitor() ). """
|
||||
self.assertFalse(visitor.exists(), "The anonymous visitor should be deleted")
|
||||
self.assertTrue(visitor.website_track_ids < main_visitor.website_track_ids)
|
||||
|
||||
def test_visitor_creation_on_tracked_page(self):
|
||||
""" Test various flows involving visitor creation and update. """
|
||||
|
||||
def authenticate(login, pwd):
|
||||
# We can't call `self.authenticate` because that tour util is
|
||||
# regenerating a new session.id before calling the real
|
||||
# `authenticate` method.
|
||||
# But we need the session id in the `authenticate` method because
|
||||
# we need to retrieve the visitor before the authentication, which
|
||||
# require the session id.
|
||||
res = self.url_open('/web/login')
|
||||
csrf_anchor = '<input type="hidden" name="csrf_token" value="'
|
||||
self.url_open('/web/login', timeout=200, data={
|
||||
'login': login,
|
||||
'password': pwd,
|
||||
'csrf_token': res.text.partition(csrf_anchor)[2].partition('"')[0],
|
||||
})
|
||||
|
||||
existing_visitors = self.env['website.visitor'].search([])
|
||||
existing_tracks = self.env['website.track'].search([])
|
||||
self.url_open(self.untracked_page.url)
|
||||
@@ -142,25 +153,25 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
new_visitor = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
new_track = self.env['website.track'].search([('id', 'not in', existing_tracks.ids)])
|
||||
self.assertEqual(len(new_visitor), 1, "1 visitor should be created")
|
||||
self.assertEqual(len(new_track), 1, "There should be 1 tracked page")
|
||||
self.assertEqual(len(new_track), 2, "There should be 2 tracked page")
|
||||
self.assertEqual(new_visitor.visit_count, 1)
|
||||
self.assertEqual(new_visitor.website_track_ids, new_track)
|
||||
self.assertVisitorTracking(new_visitor, self.tracked_page)
|
||||
self.assertVisitorTracking(new_visitor, self.tracked_page + self.tracked_page)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Admin connects
|
||||
# ------------------------------------------------------------
|
||||
|
||||
self.cookies = {'visitor_uuid': new_visitor.access_token}
|
||||
with MockRequest(self.env, website=self.website, cookies=self.cookies):
|
||||
self.authenticate(self.user_admin.login, 'admin')
|
||||
authenticate(self.user_admin.login, 'admin')
|
||||
|
||||
visitor_admin = new_visitor
|
||||
# visit a page
|
||||
self.url_open(self.tracked_page_2.url)
|
||||
|
||||
# check tracking and visitor / user sync
|
||||
self.assertVisitorTracking(visitor_admin, self.tracked_page | self.tracked_page_2)
|
||||
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
self.assertEqual(len(new_visitors), 1, "There should still be only one visitor.")
|
||||
self.assertVisitorTracking(visitor_admin, self.tracked_page + self.tracked_page + self.tracked_page_2)
|
||||
self.assertEqual(visitor_admin.partner_id, self.partner_admin)
|
||||
self.assertEqual(visitor_admin.name, self.partner_admin.name)
|
||||
|
||||
@@ -168,8 +179,8 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
# Portal connects
|
||||
# ------------------------------------------------------------
|
||||
|
||||
with MockRequest(self.env, website=self.website, cookies=self.cookies):
|
||||
self.authenticate(self.user_portal.login, 'portal')
|
||||
self.url_open('/web/session/logout')
|
||||
authenticate(self.user_portal.login, 'portal')
|
||||
|
||||
self.assertFalse(
|
||||
self.env['website.visitor'].search([('id', 'not in', (existing_visitors | visitor_admin).ids)]),
|
||||
@@ -179,7 +190,6 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
self.url_open(self.tracked_page.url)
|
||||
self.url_open(self.untracked_page.url)
|
||||
self.url_open(self.tracked_page_2.url)
|
||||
self.url_open(self.tracked_page_2.url) # 2 time to be sure it does not record twice
|
||||
|
||||
# new visitor is created
|
||||
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
@@ -194,19 +204,17 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# portal user disconnects
|
||||
self.logout()
|
||||
self.url_open('/web/session/logout')
|
||||
|
||||
# visit some pages
|
||||
self.url_open(self.tracked_page.url)
|
||||
self.url_open(self.untracked_page.url)
|
||||
self.url_open(self.tracked_page_2.url)
|
||||
self.url_open(self.tracked_page_2.url) # 2 time to be sure it does not record twice
|
||||
|
||||
# new visitor is created
|
||||
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
self.assertEqual(len(new_visitors), 3, "One extra visitor should be created")
|
||||
visitor_anonymous = new_visitors[0]
|
||||
self.cookies['visitor_uuid'] = visitor_anonymous.access_token
|
||||
self.assertFalse(visitor_anonymous.name)
|
||||
self.assertFalse(visitor_anonymous.partner_id)
|
||||
self.assertVisitorTracking(visitor_anonymous, self.tracked_page | self.tracked_page_2)
|
||||
@@ -216,37 +224,31 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
# Admin connects again
|
||||
# ------------------------------------------------------------
|
||||
|
||||
with MockRequest(self.env, website=self.website, cookies=self.cookies):
|
||||
self.authenticate(self.user_admin.login, 'admin')
|
||||
authenticate(self.user_admin.login, 'admin')
|
||||
|
||||
# one visitor is deleted
|
||||
visitor_anonymous = self.env['website.visitor'].with_context(active_test=False).search([('id', '=', visitor_anonymous.id)])
|
||||
self.assertVisitorDeactivated(visitor_anonymous, visitor_admin)
|
||||
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
self.assertEqual(new_visitors, visitor_admin | visitor_portal)
|
||||
visitor_admin = self.env['website.visitor'].search([('partner_id', '=', self.partner_admin.id)])
|
||||
# tracks are linked
|
||||
self.assertTrue(visitor_anonymous_tracks < visitor_admin.website_track_ids)
|
||||
self.assertEqual(len(visitor_admin.website_track_ids), 4, "There should be 4 tracked page for the admin")
|
||||
self.assertEqual(len(visitor_admin.website_track_ids), 5, "There should be 5 tracked page for the admin")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Back to anonymous
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# admin disconnects
|
||||
self.logout()
|
||||
self.url_open('/web/session/logout')
|
||||
|
||||
# visit some pages
|
||||
self.url_open(self.tracked_page.url)
|
||||
self.url_open(self.untracked_page.url)
|
||||
self.url_open(self.tracked_page_2.url)
|
||||
self.url_open(self.tracked_page_2.url) # 2 time to be sure it does not record twice
|
||||
|
||||
# new visitor created
|
||||
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
self.assertEqual(len(new_visitors), 3, "One extra visitor should be created")
|
||||
visitor_anonymous_2 = new_visitors[0]
|
||||
self.cookies['visitor_uuid'] = visitor_anonymous_2.access_token
|
||||
self.assertFalse(visitor_anonymous_2.name)
|
||||
self.assertFalse(visitor_anonymous_2.partner_id)
|
||||
self.assertVisitorTracking(visitor_anonymous_2, self.tracked_page | self.tracked_page_2)
|
||||
@@ -255,8 +257,7 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
# ------------------------------------------------------------
|
||||
# Portal connects again
|
||||
# ------------------------------------------------------------
|
||||
with MockRequest(self.env, website=self.website, cookies=self.cookies):
|
||||
self.authenticate(self.user_portal.login, 'portal')
|
||||
authenticate(self.user_portal.login, 'portal')
|
||||
|
||||
# one visitor is deleted
|
||||
new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)])
|
||||
@@ -276,7 +277,7 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
self.assertEqual(len(visitor_portal.website_track_ids), 5, "There should be 5 tracked page for the portal user")
|
||||
|
||||
# simulate the portal user comes back 8hours later
|
||||
visitor_portal.write({'last_connection_datetime': visitor_portal.last_connection_datetime - timedelta(hours=8)})
|
||||
visitor_portal.write({'last_connection_datetime': visitor_portal.last_connection_datetime - timedelta(hours=9)})
|
||||
self.url_open(self.tracked_page.url)
|
||||
visitor_portal.invalidate_model(['visit_count'])
|
||||
# check number of visits
|
||||
@@ -284,32 +285,32 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
|
||||
def test_clean_inactive_visitors(self):
|
||||
inactive_visitors = self.env['website.visitor'].create([{
|
||||
'name': 'Crazy Lazy Bernie',
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=8)
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=8),
|
||||
'access_token': 'f9d2b14b21be669518b14a9590cb62ed',
|
||||
}, {
|
||||
'name': 'Sleeping Joe',
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=15)
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=15),
|
||||
'access_token': 'f9d2d261a725da7f596574ca84e52f47',
|
||||
}])
|
||||
|
||||
active_visitors = self.env['website.visitor'].create([{
|
||||
'name': 'Active Donald',
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=1)
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=1),
|
||||
'access_token': 'f9d2526d9c15658bdc91d2119e54b554',
|
||||
}, {
|
||||
'name': 'Vladimir Customer',
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'partner_id': self.partner_demo.id,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=15)
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=15),
|
||||
'access_token': self.partner_demo.id,
|
||||
}])
|
||||
|
||||
self._test_unlink_old_visitors(inactive_visitors, active_visitors)
|
||||
@@ -325,7 +326,7 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
We use this method as a private tool so that sub-module can also test the cleaning of visitors
|
||||
based on their own sets of conditions. """
|
||||
|
||||
WebsiteVisitor = self.env['website.visitor'].with_context(active_test=False)
|
||||
WebsiteVisitor = self.env['website.visitor']
|
||||
|
||||
self.env['ir.config_parameter'].sudo().set_param('website.visitor.live.days', 7)
|
||||
|
||||
@@ -351,7 +352,7 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
This can happen quite often if the user switches browsers / hardwares.
|
||||
|
||||
When 'linking' visitors together, the new visitor is archived and all its relevant data is
|
||||
merged within the main visitor. See 'website.visitor#_link_to_visitor' for more details.
|
||||
merged within the main visitor. See 'website.visitor#_merge_visitor' for more details.
|
||||
|
||||
This test ensures that all the relevant data are properly merged.
|
||||
|
||||
@@ -362,16 +363,16 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
self._prepare_main_visitor_data(),
|
||||
self._prepare_linked_visitor_data()
|
||||
])
|
||||
linked_visitor._link_to_visitor(main_visitor)
|
||||
linked_visitor._merge_visitor(main_visitor)
|
||||
|
||||
self.assertVisitorDeactivated(linked_visitor, main_visitor)
|
||||
|
||||
def _prepare_main_visitor_data(self):
|
||||
return {
|
||||
'name': "Mitchell Main",
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'access_token': self.partner_admin.id,
|
||||
'website_track_ids': [(0, 0, {
|
||||
'page_id': self.tracked_page.id,
|
||||
'url': self.tracked_page.url
|
||||
@@ -380,10 +381,10 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo):
|
||||
|
||||
def _prepare_linked_visitor_data(self):
|
||||
return {
|
||||
'name': "Mitchell Main's Phone",
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'access_token': '%032x' % random.randrange(16**32),
|
||||
'website_track_ids': [(0, 0, {
|
||||
'page_id': self.tracked_page_2.id,
|
||||
'url': self.tracked_page_2.url
|
||||
|
||||
@@ -16,12 +16,13 @@ from odoo.tools.misc import DotDict, frozendict
|
||||
def MockRequest(
|
||||
env, *, path='/mockrequest/', routing=True, multilang=True,
|
||||
context=frozendict(), cookies=frozendict(), country_code=None,
|
||||
website=None, sale_order_id=None, website_sale_current_pl=None,
|
||||
website=None, remote_addr=HOST, environ_base=None,
|
||||
# website_sale
|
||||
sale_order_id=None, website_sale_current_pl=None,
|
||||
):
|
||||
|
||||
lang_code = context.get('lang', env.context.get('lang', 'en_US'))
|
||||
env = env(context=dict(context, lang=lang_code))
|
||||
|
||||
request = Mock(
|
||||
# request
|
||||
httprequest=Mock(
|
||||
@@ -31,12 +32,14 @@ def MockRequest(
|
||||
environ=dict(
|
||||
EnvironBuilder(
|
||||
path=path,
|
||||
base_url=HttpCase.base_url()
|
||||
base_url=HttpCase.base_url(),
|
||||
environ_base=environ_base,
|
||||
).get_environ(),
|
||||
REMOTE_ADDR=HOST,
|
||||
REMOTE_ADDR=remote_addr,
|
||||
),
|
||||
cookies=cookies,
|
||||
referrer='',
|
||||
remote_addr=remote_addr,
|
||||
),
|
||||
type='http',
|
||||
future_response=odoo.http.FutureResponse(),
|
||||
|
||||
@@ -212,7 +212,6 @@
|
||||
</div>
|
||||
<group id="general_info">
|
||||
<group string="Details">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="is_connected" invisible="1"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="email"/>
|
||||
@@ -271,8 +270,6 @@
|
||||
<separator/>
|
||||
<filter string="Connected" name="filter_is_connected" domain="[('last_connection_datetime', '>', datetime.datetime.now() - datetime.timedelta(minutes=5))]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="filter_is_archived" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<group string="Group By">
|
||||
<filter string="Country" name="group_by_country" context="{'group_by': 'country_id'}"/>
|
||||
<filter string="Timezone" name="group_by_timezone" context="{'group_by': 'timezone'}"/>
|
||||
|
||||
@@ -28,8 +28,8 @@ class TestBlogPerformance(UtilPerf):
|
||||
blog_tags = blog_tags[:-1]
|
||||
self.assertEqual(self._get_url_hot_query('/blog'), 11)
|
||||
self.assertEqual(self._get_url_hot_query('/blog', cache=False), 34)
|
||||
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 26)
|
||||
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 29)
|
||||
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 19)
|
||||
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url, cache=False), 22)
|
||||
|
||||
def test_30_perf_sql_blog_bigger_data_scaling(self):
|
||||
BlogPost = self.env['blog.post']
|
||||
|
||||
@@ -21,7 +21,7 @@ class Lead(models.Model):
|
||||
JOIN crm_lead_website_visitor_rel lv ON l.id = lv.crm_lead_id
|
||||
JOIN website_visitor v ON v.id = lv.website_visitor_id
|
||||
JOIN website_track p ON p.visitor_id = v.id
|
||||
WHERE l.id in %s AND v.active = TRUE
|
||||
WHERE l.id in %s
|
||||
GROUP BY l.id"""
|
||||
self.env.cr.execute(sql, (tuple(self.ids),))
|
||||
page_data = self.env.cr.dictfetchall()
|
||||
|
||||
@@ -48,14 +48,14 @@ class WebsiteVisitor(models.Model):
|
||||
domain = super()._inactive_visitors_domain()
|
||||
return expression.AND([domain, [('lead_ids', '=', False)]])
|
||||
|
||||
def _link_to_visitor(self, target):
|
||||
def _merge_visitor(self, target):
|
||||
""" Link the leads to the main visitor to avoid them being lost. """
|
||||
if self.lead_ids:
|
||||
target.write({
|
||||
'lead_ids': [(4, lead.id) for lead in self.lead_ids]
|
||||
})
|
||||
|
||||
return super(WebsiteVisitor, self)._link_to_visitor(target)
|
||||
return super()._merge_visitor(target)
|
||||
|
||||
def _prepare_message_composer_context(self):
|
||||
if not self.partner_id and self.lead_ids:
|
||||
|
||||
@@ -22,11 +22,13 @@ class TestLeadVisitorMerge(TestLeadMergeCommon):
|
||||
TestLeadMergeCommon.merge_fields.append('visitor_ids')
|
||||
|
||||
visitors = self.env['website.visitor'].sudo().create([
|
||||
{'name': 'Visitor 1',
|
||||
'lead_ids': [(4, self.lead_w_partner_company.id)],
|
||||
{
|
||||
'access_token': 'f9d2ffa0427d4e4b1d740cf5eb3cdc20',
|
||||
'lead_ids': [(4, self.lead_w_partner_company.id)],
|
||||
},
|
||||
{'name': 'Visitor 2',
|
||||
'lead_ids': [(4, self.lead_w_partner.id)],
|
||||
{
|
||||
'access_token': 'f9d2c3f741a79200487728eac989e678',
|
||||
'lead_ids': [(4, self.lead_w_partner.id)],
|
||||
}
|
||||
])
|
||||
self.assertEqual(self.lead_w_partner_company.visitor_ids, visitors[0])
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestWebsiteVisitor(TestCrmCommon, WebsiteVisitorTests):
|
||||
@users('user_sales_manager')
|
||||
def test_compute_email_phone(self):
|
||||
visitor_sudo = self.env['website.visitor'].sudo().create({
|
||||
'name': 'Mega Visitor',
|
||||
'access_token': 'f9d2c6f3b024320ac31248595ac7fcb6',
|
||||
})
|
||||
visitor = visitor_sudo.with_user(self.env.user) # as of 13.0 salesmen cannot create visitors, only read them
|
||||
customer = self.test_partner.with_user(self.env.user)
|
||||
@@ -75,11 +75,11 @@ class TestWebsiteVisitor(TestCrmCommon, WebsiteVisitorTests):
|
||||
def test_clean_inactive_visitors_crm(self):
|
||||
""" Visitors attached to leads should not be deleted even if not connected recently. """
|
||||
active_visitors = self.env['website.visitor'].create([{
|
||||
'name': 'Lead Carl',
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=8),
|
||||
'access_token': 'f9d28aad05ebee0bca215837b129aa00',
|
||||
'lead_ids': [(0, 0, {
|
||||
'name': 'Lead Carl'
|
||||
})]
|
||||
@@ -96,7 +96,7 @@ class TestWebsiteVisitor(TestCrmCommon, WebsiteVisitorTests):
|
||||
self._prepare_linked_visitor_data()
|
||||
])
|
||||
all_leads = (main_visitor + linked_visitor).lead_ids
|
||||
linked_visitor._link_to_visitor(main_visitor)
|
||||
linked_visitor._merge_visitor(main_visitor)
|
||||
|
||||
self.assertVisitorDeactivated(linked_visitor, main_visitor)
|
||||
|
||||
|
||||
@@ -287,8 +287,6 @@ class WebsiteEventController(http.Controller):
|
||||
as much informations as possible, notably to ease future communications.
|
||||
Also try to update visitor informations based on registration info. """
|
||||
visitor_sudo = request.env['website.visitor']._get_visitor_from_request(force_create=True)
|
||||
visitor_sudo._update_visitor_last_visit()
|
||||
visitor_values = {}
|
||||
|
||||
registrations_to_create = []
|
||||
for registration_values in registration_data:
|
||||
@@ -298,18 +296,11 @@ class WebsiteEventController(http.Controller):
|
||||
elif not registration_values.get('partner_id'):
|
||||
registration_values['partner_id'] = False if request.env.user._is_public() else request.env.user.partner_id.id
|
||||
|
||||
if visitor_sudo:
|
||||
# registration may give a name to the visitor, yay
|
||||
if registration_values.get('name') and not visitor_sudo.name and not visitor_values.get('name'):
|
||||
visitor_values['name'] = registration_values['name']
|
||||
# update registration based on visitor
|
||||
registration_values['visitor_id'] = visitor_sudo.id
|
||||
# update registration based on visitor
|
||||
registration_values['visitor_id'] = visitor_sudo.id
|
||||
|
||||
registrations_to_create.append(registration_values)
|
||||
|
||||
if visitor_values:
|
||||
visitor_sudo.write(visitor_values)
|
||||
|
||||
return request.env['event.registration'].sudo().create(registrations_to_create)
|
||||
|
||||
@http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True)
|
||||
|
||||
@@ -21,6 +21,16 @@ class WebsiteVisitor(models.Model):
|
||||
search="_search_event_registered_ids",
|
||||
groups="event.group_event_registration_desk")
|
||||
|
||||
@api.depends('partner_id, event_registration_ids.name')
|
||||
def name_get(self):
|
||||
""" If there is an event registration for an anonymous visitor, use that
|
||||
registered attendee name as visitor name. """
|
||||
res_dict = dict(super().name_get())
|
||||
# sudo is needed for `event_registration_ids`
|
||||
for visitor in self.sudo().filtered(lambda v: not v.partner_id and v.event_registration_ids):
|
||||
res_dict[visitor.id] = visitor.event_registration_ids[-1].name
|
||||
return list(res_dict.items())
|
||||
|
||||
@api.depends('event_registration_ids')
|
||||
def _compute_event_registration_count(self):
|
||||
if self.ids:
|
||||
@@ -46,12 +56,12 @@ class WebsiteVisitor(models.Model):
|
||||
if not visitor.mobile:
|
||||
visitor.mobile = next((reg.mobile or reg.phone for reg in linked_registrations if reg.mobile or reg.phone), False)
|
||||
|
||||
@api.depends('parent_id', 'event_registration_ids')
|
||||
@api.depends('event_registration_ids')
|
||||
def _compute_event_registered_ids(self):
|
||||
# include parent's registrations in a visitor o2m field. We don't add
|
||||
# child one as child should not have registrations (moved to the parent)
|
||||
for visitor in self:
|
||||
all_registrations = visitor.event_registration_ids | visitor.parent_id.event_registration_ids
|
||||
all_registrations = visitor.event_registration_ids
|
||||
visitor.event_registered_ids = all_registrations.mapped('event_id')
|
||||
|
||||
def _search_event_registered_ids(self, operator, operand):
|
||||
@@ -65,12 +75,7 @@ class WebsiteVisitor(models.Model):
|
||||
('event_id', operator, operand)
|
||||
])
|
||||
if all_registrations:
|
||||
# search children, even archived one, to contact them
|
||||
visitors = all_registrations.with_context(active_test=False).mapped('visitor_id')
|
||||
children = self.env['website.visitor'].with_context(
|
||||
active_test=False
|
||||
).sudo().search([('parent_id', 'in', visitors.ids)])
|
||||
visitor_ids = (visitors + children).ids
|
||||
visitor_ids = all_registrations.with_context(active_test=False).visitor_id.ids
|
||||
else:
|
||||
visitor_ids = []
|
||||
|
||||
@@ -81,16 +86,10 @@ class WebsiteVisitor(models.Model):
|
||||
domain = super()._inactive_visitors_domain()
|
||||
return expression.AND([domain, [('event_registration_ids', '=', False)]])
|
||||
|
||||
def _link_to_partner(self, partner, update_values=None):
|
||||
""" Propagate partner update to registration records """
|
||||
if partner:
|
||||
registration_wo_partner = self.event_registration_ids.filtered(lambda registration: not registration.partner_id)
|
||||
if registration_wo_partner:
|
||||
registration_wo_partner.partner_id = partner
|
||||
super(WebsiteVisitor, self)._link_to_partner(partner, update_values=update_values)
|
||||
|
||||
def _link_to_visitor(self, target):
|
||||
def _merge_visitor(self, target):
|
||||
""" Override linking process to link registrations to the final visitor. """
|
||||
self.event_registration_ids.write({'visitor_id': target.id})
|
||||
|
||||
return super(WebsiteVisitor, self)._link_to_visitor(target)
|
||||
self.event_registration_ids.visitor_id = target.id
|
||||
registration_wo_partner = self.event_registration_ids.filtered(lambda registration: not registration.partner_id)
|
||||
if registration_wo_partner:
|
||||
registration_wo_partner.partner_id = target.partner_id
|
||||
return super()._merge_visitor(target)
|
||||
|
||||
@@ -15,11 +15,11 @@ class TestEventVisitor(TestEventOnlineCommon, WebsiteVisitorTests):
|
||||
def test_clean_inactive_visitors_event(self):
|
||||
""" Visitors registered to events should not be deleted even if not connected recently. """
|
||||
active_visitors = self.env['website.visitor'].create([{
|
||||
'name': 'Lead Douglas',
|
||||
'lang_id': self.env.ref('base.lang_en').id,
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=8),
|
||||
'access_token': 'f9d2af99f543874642f89bd334fa4a49',
|
||||
'event_registration_ids': [(0, 0, {
|
||||
'event_id': self.event_0.id
|
||||
})]
|
||||
@@ -50,7 +50,7 @@ class TestEventVisitor(TestEventOnlineCommon, WebsiteVisitorTests):
|
||||
self.assertEqual(self.event_0, main_visitor.event_registered_ids)
|
||||
self.assertEqual(event_1, linked_visitor.event_registered_ids)
|
||||
|
||||
linked_visitor._link_to_visitor(main_visitor)
|
||||
linked_visitor._merge_visitor(main_visitor)
|
||||
self.assertVisitorDeactivated(linked_visitor, main_visitor)
|
||||
|
||||
# main_visitor is now attending both events
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
|
||||
<record id="website_visitor_view_search" model="ir.ui.view">
|
||||
<field name="name">website.visitor.view.search.inherit.event</field>
|
||||
<field name="model">website.visitor</field>
|
||||
<field name="inherit_id" ref="website.website_visitor_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='group_by_lang']" position="after">
|
||||
<filter string="Main Contact" name="group_by_parent_id" groups="base.group_no_one" context="{'group_by': 'parent_id'}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="website_visitor_view_tree" model="ir.ui.view">
|
||||
<field name="name">website.visitor.view.tree.inherit.event</field>
|
||||
<field name="model">website.visitor</field>
|
||||
@@ -36,9 +25,6 @@
|
||||
<field name="event_registration_count" widget="statinfo" string="Registrations"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='page_ids']" position="after">
|
||||
<field name="parent_id" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
t-att-src="image_data_uri(visitor['visitor'].partner_image) if visitor['visitor'].partner_image else '/web/static/img/user_placeholder.jpg'"
|
||||
t-att-alt="visitor['visitor'].display_name"/>
|
||||
<div class="o_we_leaderboard_name">
|
||||
<span t-if="visitor['visitor'] == current_visitor and not visitor['visitor'].name" class="font-weight-bold">You</span>
|
||||
<span t-if="visitor['visitor'] == current_visitor and not visitor['visitor'].partner_id" class="font-weight-bold">You</span>
|
||||
<span t-else="" class="font-weight-bold" t-out="visitor['visitor'].display_name"/>
|
||||
</div>
|
||||
</div></td>
|
||||
|
||||
@@ -384,7 +384,6 @@ class EventTrackController(http.Controller):
|
||||
track = self._fetch_track(track_id, allow_sudo=True)
|
||||
force_create = set_reminder_on or track.wishlisted_by_default
|
||||
event_track_partner = track._get_event_track_visitors(force_create=force_create)
|
||||
visitor_sudo = event_track_partner.visitor_id
|
||||
|
||||
if not track.wishlisted_by_default:
|
||||
if not event_track_partner or event_track_partner.is_wishlisted == set_reminder_on: # ignore if new state = old state
|
||||
@@ -396,8 +395,6 @@ class EventTrackController(http.Controller):
|
||||
event_track_partner.is_blacklisted = not set_reminder_on
|
||||
|
||||
result = {'reminderOn': set_reminder_on}
|
||||
if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
|
||||
result['visitor_uuid'] = visitor_sudo.access_token
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -53,15 +53,10 @@ class WebsiteVisitor(models.Model):
|
||||
domain = super()._inactive_visitors_domain()
|
||||
return expression.AND([domain, [('event_track_visitor_ids', '=', False)]])
|
||||
|
||||
def _link_to_partner(self, partner, update_values=None):
|
||||
""" Propagate partner update to track_visitor records """
|
||||
if partner:
|
||||
track_visitor_wo_partner = self.event_track_visitor_ids.filtered(lambda track_visitor: not track_visitor.partner_id)
|
||||
if track_visitor_wo_partner:
|
||||
track_visitor_wo_partner.partner_id = partner
|
||||
super(WebsiteVisitor, self)._link_to_partner(partner, update_values=update_values)
|
||||
|
||||
def _link_to_visitor(self, target):
|
||||
def _merge_visitor(self, target):
|
||||
""" Override linking process to link wishlist to the final visitor. """
|
||||
self.event_track_visitor_ids.visitor_id = target.id
|
||||
return super(WebsiteVisitor, self)._link_to_visitor(target)
|
||||
track_visitor_wo_partner = self.event_track_visitor_ids.filtered(lambda track_visitor: not track_visitor.partner_id)
|
||||
if track_visitor_wo_partner:
|
||||
track_visitor_wo_partner.partner_id = target.partner_id
|
||||
return super()._merge_visitor(target)
|
||||
|
||||
@@ -3,7 +3,6 @@ odoo.define('website_event_track.website_event_track_reminder', function (requir
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var utils = require('web.utils');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
publicWidget.registry.websiteEventTrackReminder = publicWidget.Widget.extend({
|
||||
@@ -71,9 +70,6 @@ publicWidget.registry.websiteEventTrackReminder = publicWidget.Widget.extend({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (result.visitor_uuid) {
|
||||
utils.set_cookie('visitor_uuid', result.visitor_uuid);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -145,8 +145,8 @@ class TestTrackSuggestions(TestEventOnlineCommon):
|
||||
}])
|
||||
|
||||
emp_visitor = self.env['website.visitor'].create({
|
||||
'name': 'Visitor',
|
||||
'partner_id': self.user_employee.partner_id.id
|
||||
'partner_id': self.user_employee.partner_id.id,
|
||||
'access_token': self.user_employee.partner_id.id,
|
||||
})
|
||||
visitor_track = self.env['event.track.visitor'].create({
|
||||
'visitor_id': emp_visitor.id,
|
||||
|
||||
@@ -25,6 +25,7 @@ class WebsiteVisitorTestsEventTrack(TestEventOnlineCommon, WebsiteVisitorTests):
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
'website_id': 1,
|
||||
'last_connection_datetime': datetime.now() - timedelta(days=8),
|
||||
'access_token': 'f9d2b93591d6f602e5e8afa238e35a6c',
|
||||
'event_track_visitor_ids': [(0, 0, {
|
||||
'track_id': track_1.id,
|
||||
'is_wishlisted': True
|
||||
@@ -60,7 +61,7 @@ class WebsiteVisitorTestsEventTrack(TestEventOnlineCommon, WebsiteVisitorTests):
|
||||
'is_wishlisted': True,
|
||||
}])
|
||||
|
||||
linked_visitor._link_to_visitor(main_visitor)
|
||||
linked_visitor._merge_visitor(main_visitor)
|
||||
|
||||
self.assertVisitorDeactivated(linked_visitor, main_visitor)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ class WebsiteEventTrackQuiz(EventTrackController):
|
||||
track_sudo = track.sudo()
|
||||
|
||||
event_track_visitor = track._get_event_track_visitors(force_create=True)
|
||||
visitor_sudo = event_track_visitor.visitor_id
|
||||
if event_track_visitor.quiz_completed:
|
||||
return {'error': 'track_quiz_done'}
|
||||
|
||||
@@ -45,8 +44,6 @@ class WebsiteEventTrackQuiz(EventTrackController):
|
||||
'quiz_completed': event_track_visitor.quiz_completed,
|
||||
'quiz_points': answers_details['points']
|
||||
}
|
||||
if visitor_sudo and request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
|
||||
result['visitor_uuid'] = visitor_sudo.access_token
|
||||
return result
|
||||
|
||||
@http.route('/event_track/quiz/reset', type="json", auth="public", website=True)
|
||||
|
||||
@@ -5,7 +5,6 @@ odoo.define('website_event_track_quiz.event.quiz', function (require) {
|
||||
var publicWidget = require('web.public.widget');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
@@ -235,9 +234,6 @@ var Quiz = publicWidget.Widget.extend({
|
||||
}
|
||||
self._renderAnswersHighlightingAndComments();
|
||||
self._renderValidationInfo();
|
||||
if (data.visitor_uuid) {
|
||||
utils.set_cookie('visitor_uuid', data.visitor_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
t-att-src="image_data_uri(visitor['visitor'].partner_image) if visitor['visitor'].partner_image else '/web/static/img/user_placeholder.jpg'"/>
|
||||
<img class="position-absolute" t-attf-src="/website_profile/static/src/img/rank_#{visitor['position']}.svg" alt="User rank" style="bottom: 0; right: -10px"/>
|
||||
</div>
|
||||
<h3 t-if="visitor['visitor'] == current_visitor and not visitor['visitor'].name" class="mt-2 mb-0">You</h3>
|
||||
<h3 t-if="visitor['visitor'] == current_visitor and not visitor['visitor'].partner_id" class="mt-2 mb-0">You</h3>
|
||||
<h3 t-else="" class="mt-2 mb-0" t-out="visitor['visitor'].display_name"/>
|
||||
</div>
|
||||
<div class="row mx-0 o_wprofile_top3_card_footer text-nowrap">
|
||||
@@ -81,7 +81,7 @@
|
||||
t-att-src="image_data_uri(visitor['visitor'].partner_image) if visitor['visitor'].partner_image else '/web/static/img/user_placeholder.jpg'"/>
|
||||
</td>
|
||||
<td class="align-middle w-md-75">
|
||||
<span t-if="visitor['visitor'] == current_visitor and not visitor['visitor'].name" class="font-weight-bold">You</span>
|
||||
<span t-if="visitor['visitor'] == current_visitor and not visitor['visitor'].partner_id" class="font-weight-bold">You</span>
|
||||
<span t-else="" class="font-weight-bold" t-out="visitor['visitor'].display_name"/><br/>
|
||||
</td>
|
||||
<td class="align-middle font-weight-bold text-right text-nowrap">
|
||||
|
||||
@@ -92,28 +92,24 @@ class WebsiteVisitor(models.Model):
|
||||
notifications.append([operator.partner_id, 'website_livechat.send_chat_request', mail_channel_info])
|
||||
self.env['bus.bus']._sendmany(notifications)
|
||||
|
||||
def _link_to_visitor(self, target):
|
||||
def _merge_visitor(self, target):
|
||||
""" Copy sessions of the secondary visitors to the main partner visitor. """
|
||||
if target.partner_id:
|
||||
target.mail_channel_ids |= self.mail_channel_ids
|
||||
super(WebsiteVisitor, self)._link_to_visitor(target)
|
||||
target.mail_channel_ids |= self.mail_channel_ids
|
||||
self.mail_channel_ids.channel_partner_ids = [
|
||||
(3, self.env.ref('base.public_partner').id),
|
||||
(4, target.partner_id.id),
|
||||
]
|
||||
return super()._merge_visitor(target)
|
||||
|
||||
def _link_to_partner(self, partner, update_values=None):
|
||||
""" Adapt partner in members of related livechats """
|
||||
if partner:
|
||||
self.mail_channel_ids.channel_partner_ids = [
|
||||
(3, self.env.ref('base.public_partner').id),
|
||||
(4, partner.id),
|
||||
]
|
||||
super(WebsiteVisitor, self)._link_to_partner(partner, update_values=update_values)
|
||||
|
||||
def _create_visitor(self):
|
||||
visitor = super(WebsiteVisitor, self)._create_visitor()
|
||||
mail_channel_uuid = json.loads(request.httprequest.cookies.get('im_livechat_session', '{}')).get('uuid')
|
||||
if mail_channel_uuid:
|
||||
mail_channel = request.env["mail.channel"].sudo().search([("uuid", "=", mail_channel_uuid)])
|
||||
mail_channel.write({
|
||||
'livechat_visitor_id': visitor.id,
|
||||
'anonymous_name': visitor.display_name
|
||||
})
|
||||
return visitor
|
||||
def _upsert_visitor(self, access_token, force_track_values=None):
|
||||
visitor_id, upsert = super()._upsert_visitor(access_token, force_track_values=force_track_values)
|
||||
if upsert == 'inserted':
|
||||
visitor_sudo = self.sudo().browse(visitor_id)
|
||||
mail_channel_uuid = json.loads(request.httprequest.cookies.get('im_livechat_session', '{}')).get('uuid')
|
||||
if mail_channel_uuid:
|
||||
mail_channel = request.env["mail.channel"].sudo().search([("uuid", "=", mail_channel_uuid)])
|
||||
mail_channel.write({
|
||||
'livechat_visitor_id': visitor_sudo.id,
|
||||
'anonymous_name': visitor_sudo.display_name
|
||||
})
|
||||
return visitor_id, upsert
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import random
|
||||
|
||||
from odoo import fields, tests
|
||||
|
||||
|
||||
@@ -39,7 +41,11 @@ class TestLivechatCommon(tests.TransactionCase):
|
||||
'country_id': self.env.ref('base.de').id,
|
||||
'website_id': self.env.ref('website.default_website').id,
|
||||
'partner_id': self.env.ref('base.user_demo').partner_id.id,
|
||||
}] + [visitor_vals]*self.max_sessions_per_operator)
|
||||
'access_token': self.env.ref('base.user_demo').partner_id.id,
|
||||
}] + [
|
||||
dict(visitor_vals, access_token='%032x' % random.randrange(16**32))
|
||||
for _ in range(self.max_sessions_per_operator)
|
||||
])
|
||||
self.visitor_demo, self.visitor = self.visitors[0], self.visitors[1]
|
||||
|
||||
base_url = self.livechat_channel.get_base_url()
|
||||
|
||||
@@ -10,7 +10,7 @@ class TestLivechatUI(tests.HttpCase, TestLivechatCommon):
|
||||
def setUp(self):
|
||||
super(TestLivechatUI, self).setUp()
|
||||
self.visitor_tour = self.env['website.visitor'].create({
|
||||
'name': 'Visitor Tour',
|
||||
'access_token': 'f9d2e784d3d96a904fca2f5e2a559a19',
|
||||
'website_id': self.env.ref('website.default_website').id,
|
||||
})
|
||||
self.target_visitor = self.visitor_tour
|
||||
|
||||
@@ -16,7 +16,7 @@ class WebsiteVisitorTestsLivechat(WebsiteVisitorTests):
|
||||
self._prepare_linked_visitor_data()
|
||||
])
|
||||
all_mail_channels = (main_visitor + linked_visitor).mail_channel_ids
|
||||
linked_visitor._link_to_visitor(main_visitor)
|
||||
linked_visitor._merge_visitor(main_visitor)
|
||||
|
||||
self.assertVisitorDeactivated(linked_visitor, main_visitor)
|
||||
|
||||
|
||||
@@ -1241,10 +1241,7 @@ class WebsiteSale(http.Controller):
|
||||
def products_recently_viewed_update(self, product_id, **kwargs):
|
||||
res = {}
|
||||
visitor_sudo = request.env['website.visitor']._get_visitor_from_request(force_create=True)
|
||||
if visitor_sudo:
|
||||
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)
|
||||
visitor_sudo._add_viewed_product(product_id)
|
||||
return res
|
||||
|
||||
@http.route('/shop/products/recently_viewed_delete', type='json', auth='public', website=True)
|
||||
|
||||
@@ -44,5 +44,5 @@ class WebsiteVisitor(models.Model):
|
||||
self.ensure_one()
|
||||
if product_id and self.env['product.product'].browse(product_id)._is_variant_possible():
|
||||
domain = [('product_id', '=', product_id)]
|
||||
website_track_values = {'product_id': product_id, 'visit_datetime': datetime.now()}
|
||||
website_track_values = {'product_id': product_id}
|
||||
self._add_tracking(domain, website_track_values)
|
||||
|
||||
@@ -45,9 +45,6 @@ publicWidget.registry.productsRecentlyViewedUpdate = publicWidget.Widget.extend(
|
||||
product_id: productId,
|
||||
}
|
||||
}).then(function (res) {
|
||||
if (res && res.visitor_uuid) {
|
||||
utils.set_cookie('visitor_uuid', res.visitor_uuid);
|
||||
}
|
||||
utils.set_cookie(cookieName, productId, 30 * 60);
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user