[FW][FIX] adapt odoo 15.0 to work with ubuntu jammy

Ubuntu Jammy brings some breaking changes for odoo 15.0. Since 15.0 is still the main branch, this pr aims to ensure compatibility if deployed on ubuntu Jammy.

Main changes:
- python 3.10
- werkzeug 2.0+, including vendoring werkzeug's user agent parser in `odoo/tools/_vendor/useragents.py` (= the one from version 0.16) as it is discontinued after v2.1
- currentThread is now deprecated (use current_thread instead)
- default requirement version (based on deb package version)
- new opcode in python 3.10
- distutils.version.LooseVersion is deprecated

- wkhtmltopd version packaged with ubuntu jammy don't have patched qt but will work without headers and footers. Suggested wkhtmltopdf version 0.12.5 doesn't have a build working with jammy yet because of incompatible libssl dependencies.  A parallel wip may provide a 0.12.5 version compatible with jammy latter (currently used as testing version on runbot). If this solution is chosen, this build may be provided by odoo latter.

As usual, all changes are made with the spirit to keep compatibility with previous version (Focal)

enterprise PR: https://github.com/odoo/enterprise/pull/26831

closes #84097
closes #88980
closes #89533
closes #78020
closes #83309
closes #87463

closes odoo/odoo#91927

Forward-port-of: odoo/odoo#91543
Forward-port-of: odoo/odoo#88803
Related: odoo/enterprise#27601
Signed-off-by: Christophe Monniez (moc) <moc@odoo.com>
This commit is contained in:
Odoo's Mergebot
2022-05-23 08:29:56 +02:00
committed by GitHub
35 changed files with 275 additions and 62 deletions
+1 -1
View File
@@ -2156,7 +2156,7 @@ class Lead(models.Model):
# - avoid blocking the table for too long with a too big transaction
transactions_count, transactions_failed_count = 0, 0
cron_update_lead_start_date = datetime.now()
auto_commit = not getattr(threading.currentThread(), 'testing', False)
auto_commit = not getattr(threading.current_thread(), 'testing', False)
for probability, probability_lead_ids in probability_leads.items():
for lead_ids_current in tools.split_every(PLS_UPDATE_BATCH_STEP, probability_lead_ids):
transactions_count += 1
+1 -1
View File
@@ -445,7 +445,7 @@ class Team(models.Model):
BUNDLE_HOURS_DELAY = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.delay', default=0))
BUNDLE_COMMIT_SIZE = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.commit.bundle', 100))
auto_commit = not getattr(threading.currentThread(), 'testing', False)
auto_commit = not getattr(threading.current_thread(), 'testing', False)
# leads
max_create_dt = self.env.cr.now() - datetime.timedelta(hours=BUNDLE_HOURS_DELAY)
+1 -1
View File
@@ -154,7 +154,7 @@ class TeamMember(models.Model):
leads_done_ids = set()
counter = 0
# auto-commit except in testing mode
auto_commit = not getattr(threading.currentThread(), 'testing', False)
auto_commit = not getattr(threading.current_thread(), 'testing', False)
commit_bundle_size = int(self.env['ir.config_parameter'].sudo().get_param('crm.assignment.commit.bundle', 100))
while population:
counter += 1
+1 -1
View File
@@ -242,7 +242,7 @@ You receive this email because you are:
self.invalidate_cache()
self._warn_template_error(scheduler, e)
else:
if autocommit and not getattr(threading.currentThread(), 'testing', False):
if autocommit and not getattr(threading.current_thread(), 'testing', False):
self.env.cr.commit()
return True
+1 -1
View File
@@ -23,7 +23,7 @@ class ResConfigSettings(models.TransientModel):
@api.constrains('module_hr_presence', 'hr_presence_control_email', 'hr_presence_control_ip')
def _check_advanced_presence(self):
test_mode = self.env.registry.in_test_mode() or getattr(threading.currentThread(), 'testing', False)
test_mode = self.env.registry.in_test_mode() or getattr(threading.current_thread(), 'testing', False)
if self.env.context.get('install_mode', False) or test_mode:
return
@@ -4,7 +4,8 @@
import re
import markupsafe
from werkzeug import urls, utils
from html import unescape
from werkzeug import urls
from odoo import api, models, tools
@@ -45,7 +46,7 @@ class MailRenderMixin(models.AbstractModel):
continue
label = (match[3] or '').strip()
create_vals = dict(link_tracker_vals, url=utils.unescape(long_url), label=utils.unescape(label))
create_vals = dict(link_tracker_vals, url=unescape(long_url), label=unescape(label))
link = self.env['link.tracker'].search_or_create(create_vals)
if link.short_url:
# `str` manipulation required to support replacing "&" characters, common in urls
@@ -75,7 +76,7 @@ class MailRenderMixin(models.AbstractModel):
if blacklist and any(item in parsed.path for item in blacklist):
continue
create_vals = dict(link_tracker_vals, url=utils.unescape(original_url))
create_vals = dict(link_tracker_vals, url=unescape(original_url))
link = self.env['link.tracker'].search_or_create(create_vals)
if link.short_url:
# Ensures we only replace the same link and not a subpart of a longer one, multiple times if applicable
+1 -1
View File
@@ -197,7 +197,7 @@ class MailMail(models.Model):
res = None
try:
# auto-commit except in testing mode
auto_commit = not getattr(threading.currentThread(), 'testing', False)
auto_commit = not getattr(threading.current_thread(), 'testing', False)
res = self.browse(ids).send(auto_commit=auto_commit)
except Exception:
_logger.exception("Failed processing mail queue")
+1 -1
View File
@@ -2480,7 +2480,7 @@ class MailThread(models.AbstractModel):
# 2. do not send emails immediately if the registry is not loaded,
# to prevent sending email during a simple update of the database
# using the command-line.
test_mode = getattr(threading.currentThread(), 'testing', False)
test_mode = getattr(threading.current_thread(), 'testing', False)
if force_send and len(emails) < recipients_max and (not self.pool._init or test_mode):
# unless asked specifically, send emails after the transaction to
# avoid side effects due to emails being sent while the transaction fails
+1 -1
View File
@@ -978,7 +978,7 @@ class MassMailing(models.Model):
extra_context = mailing._get_mass_mailing_context()
composer = composer.with_context(active_ids=res_ids, **extra_context)
# auto-commit except in testing mode
auto_commit = not getattr(threading.currentThread(), 'testing', False)
auto_commit = not getattr(threading.current_thread(), 'testing', False)
composer._action_send_mail(auto_commit=auto_commit)
mailing.write({
'state': 'done',
@@ -27,7 +27,7 @@ class ResCompany(models.Model):
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
if not getattr(threading.currentThread(), 'testing', False):
if not getattr(threading.current_thread(), 'testing', False):
res.iap_enrich_auto()
return res
@@ -4,7 +4,8 @@
from datetime import date, timedelta
import requests
import werkzeug
from html import unescape
from odoo import models, api, service
from odoo.tools.translate import _
@@ -69,7 +70,7 @@ class MercuryTransaction(models.Model):
try:
r = requests.post(url, data=xml_transaction, headers=headers, timeout=65)
r.raise_for_status()
response = werkzeug.utils.unescape(r.content.decode())
response = unescape(r.content.decode())
except Exception:
response = "timeout"
+2 -2
View File
@@ -104,7 +104,7 @@ class ProductTemplate(models.Model):
def write(self, vals):
# timesheet product can't be archived
test_mode = getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode()
test_mode = getattr(threading.current_thread(), 'testing', False) or self.env.registry.in_test_mode()
if not test_mode and 'active' in vals and not vals['active']:
time_product = self.env.ref('sale_timesheet.time_product')
if time_product.product_tmpl_id in self:
@@ -138,7 +138,7 @@ class ProductProduct(models.Model):
def write(self, vals):
# timesheet product can't be archived
test_mode = getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode()
test_mode = getattr(threading.current_thread(), 'testing', False) or self.env.registry.in_test_mode()
if not test_mode and 'active' in vals and not vals['active']:
time_product = self.env.ref('sale_timesheet.time_product')
if time_product in self:
+2 -2
View File
@@ -96,7 +96,7 @@ class SmsSms(models.Model):
for batch_ids in self._split_batch():
self.browse(batch_ids)._send(unlink_failed=unlink_failed, unlink_sent=unlink_sent, raise_exception=raise_exception)
# auto-commit if asked except in testing mode
if auto_commit is True and not getattr(threading.currentThread(), 'testing', False):
if auto_commit is True and not getattr(threading.current_thread(), 'testing', False):
self._cr.commit()
def resend_failed(self):
@@ -145,7 +145,7 @@ class SmsSms(models.Model):
res = None
try:
# auto-commit except in testing mode
auto_commit = not getattr(threading.currentThread(), 'testing', False)
auto_commit = not getattr(threading.current_thread(), 'testing', False)
res = self.browse(ids).send(unlink_failed=False, unlink_sent=True, auto_commit=auto_commit, raise_exception=False)
except Exception:
_logger.exception("Failed processing SMS queue")
+2 -2
View File
@@ -23,7 +23,7 @@ class Picking(models.Model):
is_delivery = picking.company_id.stock_move_sms_validation \
and picking.picking_type_id.code == 'outgoing' \
and (picking.partner_id.mobile or picking.partner_id.phone)
if is_delivery and not getattr(threading.currentThread(), 'testing', False) \
if is_delivery and not getattr(threading.current_thread(), 'testing', False) \
and not self.env.registry.in_test_mode() \
and not picking.company_id.has_received_warning_stock_sms \
and picking.company_id.stock_move_sms_validation:
@@ -52,7 +52,7 @@ class Picking(models.Model):
def _send_confirmation_email(self):
super(Picking, self)._send_confirmation_email()
if not self.env.context.get('skip_sms') and not getattr(threading.currentThread(), 'testing', False) and not self.env.registry.in_test_mode():
if not self.env.context.get('skip_sms') and not getattr(threading.current_thread(), 'testing', False) and not self.env.registry.in_test_mode():
pickings = self.filtered(lambda p: p.company_id.stock_move_sms_validation and p.picking_type_id.code == 'outgoing' and (p.partner_id.mobile or p.partner_id.phone))
for picking in pickings:
# Sudo as the user has not always the right to read this sms template.
@@ -42,7 +42,7 @@ class TestCrmPerformance(CrmPerformanceCase):
country_be = self.env.ref('base.be')
lang_be_id = self.env['res.lang']._lang_get_id('fr_BE')
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=243): # 235, sometimes +6
with freeze_time(self.reference_now), self.assertQueryCount(user_sales_leads=244): # 235, sometimes +6
self.env.cr._now = self.reference_now # force create_date to check schedulers
crm_values = [
{'country_id': country_be.id,
+1 -1
View File
@@ -346,7 +346,7 @@ class IrUiView(models.Model):
# During a theme module update, theme views' copies receiving an arch
# update should not be considered as `arch_updated`, as this is not a
# user made change.
test_mode = getattr(threading.currentThread(), 'testing', False)
test_mode = getattr(threading.current_thread(), 'testing', False)
if not (test_mode or self.pool._init):
return super().write(vals)
no_arch_updated_views = other_views = self.env['ir.ui.view']
+1 -1
View File
@@ -100,7 +100,7 @@ def registry(database_name=None):
"""
if database_name is None:
import threading
database_name = threading.currentThread().dbname
database_name = threading.current_thread().dbname
return modules.registry.Registry(database_name)
#----------------------------------------------------------
+3 -4
View File
@@ -6,7 +6,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import UserError, AccessError
from odoo.tools.safe_eval import safe_eval, time
from odoo.tools.misc import find_in_path
from odoo.tools import config, is_html_empty
from odoo.tools import config, is_html_empty, parse_version
from odoo.http import request
from odoo.osv.expression import NEGATIVE_TERM_OPERATORS, FALSE_DOMAIN
@@ -21,7 +21,6 @@ import json
from lxml import etree
from contextlib import closing
from distutils.version import LooseVersion
from reportlab.graphics.barcode import createBarcodeDrawing
from PyPDF2 import PdfFileWriter, PdfFileReader, utils
from collections import OrderedDict
@@ -69,12 +68,12 @@ else:
match = re.search(b'([0-9.]+)', out)
if match:
version = match.group(0).decode('ascii')
if LooseVersion(version) < LooseVersion('0.12.0'):
if parse_version(version) < parse_version('0.12.0'):
_logger.info('Upgrade Wkhtmltopdf to (at least) 0.12.0')
wkhtmltopdf_state = 'upgrade'
else:
wkhtmltopdf_state = 'ok'
if LooseVersion(version) >= LooseVersion('0.12.2'):
if parse_version(version) >= parse_version('0.12.2'):
wkhtmltopdf_dpi_zoom_ratio = True
if config['workers'] == 1:
+1 -1
View File
@@ -711,7 +711,7 @@ class IrMailServer(models.Model):
Can be overridden in tests after mocking the SMTP lib to test in depth the
outgoing mail server.
"""
return getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode()
return getattr(threading.current_thread(), 'testing', False) or self.env.registry.in_test_mode()
def _neutralize(self):
super()._neutralize()
+1 -1
View File
@@ -578,7 +578,7 @@ class Module(models.Model):
}
def _button_immediate_function(self, function):
if getattr(threading.currentThread(), 'testing', False):
if getattr(threading.current_thread(), 'testing', False):
raise RuntimeError(
"Module operations inside tests are not transactional and thus forbidden.\n"
"If you really need to perform module operations to test a specific behavior, it "
+1 -1
View File
@@ -512,6 +512,6 @@ class TestEmailMessage(TransactionCase):
msg['References'] = '<345227342212345.1596730777.324691772483620-example-30453-other.reference@test-123.example.com>'
smtp = FakeSMTP()
self.patch(threading.currentThread(), 'testing', False)
self.patch(threading.current_thread(), 'testing', False)
self.env['ir.mail_server'].send_email(msg, smtp_session=smtp)
self.assertTrue(smtp.email_sent)
+3 -1
View File
@@ -1,5 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import html
import json
from unittest.mock import patch
from urllib.parse import urlparse
@@ -380,7 +381,7 @@ class TestHttpModels(TestHttpBase):
milky_way = self.env.ref('test_http.milky_way')
res = self.url_open(f'/test_http/{milky_way.id}/9999') # unknown gate
self.assertEqual(res.status_code, 400)
self.assertIn("The goa'uld destroyed the gate", res.text)
self.assertIn("The goa'uld destroyed the gate", html.unescape(res.text))
@tagged('post_install', '-at_install')
@@ -510,6 +511,7 @@ class TestHttpEnsureDb(TestHttpBase):
self.assertEqual(new_session.uid, None)
# follow redirection
self.opener.cookies['session_id'] = new_session.sid
res = self.multidb_url_open('/test_http/ensure_db')
res.raise_for_status()
self.assertEqual(res.status_code, 200)
+3 -4
View File
@@ -7,7 +7,6 @@ try:
except ImportError:
pylint = None
import subprocess
from distutils.version import LooseVersion
import os
from os.path import join
import sys
@@ -53,10 +52,10 @@ class TestPyLint(TransactionCase):
def test_pylint(self):
if pylint is None:
self._skip_test('please install pylint')
required_pylint_version = LooseVersion('1.6.4')
required_pylint_version = tools.parse_version('1.6.4')
if sys.version_info >= (3, 6):
required_pylint_version = LooseVersion('1.7.0')
if LooseVersion(getattr(pylint, '__version__', '0.0.1')) < required_pylint_version:
required_pylint_version = tools.parse_version('1.7.0')
if tools.parse_version(getattr(pylint, '__version__', '0.0.1')) < required_pylint_version:
self._skip_test('please upgrade pylint to >= %s' % required_pylint_version)
paths = [tools.config['root_path']]
+3 -1
View File
@@ -160,6 +160,7 @@ from .tools.geoipresolver import GeoIPResolver
from .tools.func import filter_kwargs, lazy_property
from .tools.mimetypes import guess_mimetype
from .tools._vendor import sessions
from .tools._vendor.useragents import UserAgent
_logger = logging.getLogger(__name__)
@@ -888,7 +889,7 @@ class Response(werkzeug.wrappers.Response):
_logger.warning("%s returns an HTTPException instead of raising it.", fname)
raise result
if isinstance(result, werkzeug.wrappers.BaseResponse):
if isinstance(result, werkzeug.wrappers.Response):
response = cls.force_type(result)
response.set_default()
return response
@@ -1752,6 +1753,7 @@ class Application:
return response(environ, start_response)
httprequest = werkzeug.wrappers.Request(environ)
httprequest.user_agent_class = UserAgent # use vendored userAgent since it will be removed in 2.1
httprequest.parameter_storage_class = (
werkzeug.datastructures.ImmutableOrderedMultiDict)
request = Request(httprequest)
+2 -2
View File
@@ -62,7 +62,7 @@ def load_data(cr, idref, mode, kind, package):
filename = None
try:
if kind in ('demo', 'test'):
threading.currentThread().testing = True
threading.current_thread().testing = True
for filename in _get_files_of_kind(kind):
_logger.info("loading %s/%s", package.name, filename)
noupdate = False
@@ -71,7 +71,7 @@ def load_data(cr, idref, mode, kind, package):
tools.convert_file(cr, package.name, filename, idref, mode, noupdate, kind)
finally:
if kind in ('demo', 'test'):
threading.currentThread().testing = False
threading.current_thread().testing = False
return bool(filename)
+1 -1
View File
@@ -128,7 +128,7 @@ def init_logger():
warnings.simplefilter('default', category=DeprecationWarning)
# ignore deprecation warnings from invalid escape (there's a ton and it's
# pretty likely a super low-value signal)
warnings.filterwarnings('ignore', r'^invalid escape sequence \\.', category=DeprecationWarning)
warnings.filterwarnings('ignore', r'^invalid escape sequence \'?\\.', category=DeprecationWarning)
# recordsets are both sequence and set so trigger warning despite no issue
warnings.filterwarnings('ignore', r'^Sampling from a set', category=DeprecationWarning, module='odoo')
# ignore a bunch of warnings we can't really fix ourselves
+12 -12
View File
@@ -123,7 +123,7 @@ class RequestHandler(werkzeug.serving.WSGIRequestHandler):
self.timeout = 5
# flag the current thread as handling a http request
super(RequestHandler, self).setup()
me = threading.currentThread()
me = threading.current_thread()
me.name = 'odoo.service.http.request.%s' % (me.ident,)
@@ -290,7 +290,7 @@ class FSWatcherInotify(FSWatcherBase):
def start(self):
self.started = True
self.thread = threading.Thread(target=self.run, name="odoo.service.autoreload.watcher")
self.thread.setDaemon(True)
self.thread.daemon = True
self.thread.start()
def stop(self):
@@ -350,7 +350,7 @@ class CommonServer(object):
class ThreadedServer(CommonServer):
def __init__(self, app):
super(ThreadedServer, self).__init__(app)
self.main_thread_id = threading.currentThread().ident
self.main_thread_id = threading.current_thread().ident
# Variable keeping track of the number of calls to the signal handler defined
# below. This variable is monitored by ``quit_on_signals()``.
self.quit_signals_received = 0
@@ -385,7 +385,7 @@ class ThreadedServer(CommonServer):
memory = memory_info(psutil.Process(os.getpid()))
if config['limit_memory_soft'] and memory > config['limit_memory_soft']:
_logger.warning('Server memory limit (%s) reached.', memory)
self.limits_reached_threads.add(threading.currentThread())
self.limits_reached_threads.add(threading.current_thread())
for thread in threading.enumerate():
if not thread.daemon or getattr(thread, 'type', None) == 'cron':
@@ -448,7 +448,7 @@ class ThreadedServer(CommonServer):
_logger.debug('cron%d polling for jobs', number)
for db_name, registry in registries.d.items():
if registry.ready:
thread = threading.currentThread()
thread = threading.current_thread()
thread.start_time = time.time()
try:
ir_cron._process_jobs(db_name)
@@ -472,7 +472,7 @@ class ThreadedServer(CommonServer):
def target():
self.cron_thread(i)
t = threading.Thread(target=target, name="odoo.service.cron.cron%d" % i)
t.setDaemon(True)
t.daemon = True
t.type = 'cron'
t.start()
_logger.debug("cron%d started!" % i)
@@ -483,7 +483,7 @@ class ThreadedServer(CommonServer):
def http_spawn(self):
t = threading.Thread(target=self.http_thread, name="odoo.service.httpd")
t.setDaemon(True)
t.daemon = True
t.start()
def start(self, stop=False):
@@ -525,11 +525,11 @@ class ThreadedServer(CommonServer):
# Manually join() all threads before calling sys.exit() to allow a second signal
# to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
# threading.Thread.join() should not mask signals (at least in python 2.5).
me = threading.currentThread()
me = threading.current_thread()
_logger.debug('current thread: %r', me)
for thread in threading.enumerate():
_logger.debug('process %r (%r)', thread, thread.isDaemon())
if (thread != me and not thread.isDaemon() and thread.ident != self.main_thread_id and
_logger.debug('process %r (%r)', thread, thread.daemon)
if (thread != me and not thread.daemon and thread.ident != self.main_thread_id and
thread not in self.limits_reached_threads):
while thread.is_alive() and (time.time() - stop_time) < 1:
# We wait for requests to finish, up to 1 second.
@@ -1217,7 +1217,7 @@ def _reexec(updated_modules=None):
def load_test_file_py(registry, test_file):
from odoo.tests.common import OdooSuite
threading.currentThread().testing = True
threading.current_thread().testing = True
try:
test_path, _ = os.path.splitext(os.path.abspath(test_file))
for mod in [m for m in get_modules() if '%s%s%s' % (os.path.sep, m, os.path.sep) in test_file]:
@@ -1233,7 +1233,7 @@ def load_test_file_py(registry, test_file):
_logger.error('%s: at least one error occurred in a test', test_file)
return
finally:
threading.currentThread().testing = False
threading.current_thread().testing = False
def preload_registries(dbnames):
""" Preload a registries, possibly run a test file."""
+2 -2
View File
@@ -72,12 +72,12 @@ def run_suite(suite, module_name=None):
# avoid dependency hell
from ..modules import module
module.current_test = module_name
threading.currentThread().testing = True
threading.current_thread().testing = True
results = OdooTestResult()
suite(results)
threading.currentThread().testing = False
threading.current_thread().testing = False
module.current_test = None
return results
+1 -2
View File
@@ -20,14 +20,13 @@ import os
import re
import tempfile
from hashlib import sha1
from os import path
from os import path, replace as rename
from pickle import dump
from pickle import HIGHEST_PROTOCOL
from pickle import load
from time import time
from werkzeug.datastructures import CallbackDict
from werkzeug.posixemulation import rename
_logger = logging.getLogger(__name__)
_sha1_re = re.compile(r"^[a-f0-9]{40}$")
+204
View File
@@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
"""
werkzeug.useragents
~~~~~~~~~~~~~~~~~~~
This module provides a helper to inspect user agent strings. This module
is far from complete but should work for most of the currently available
browsers.
:copyright: 2007 Pallets
:license: BSD-3-Clause
This package was vendored in odoo in order to prevent errors with werkzeug 2.1
"""
import re
class UserAgentParser(object):
"""A simple user agent parser. Used by the `UserAgent`."""
platforms = (
("cros", "chromeos"),
("iphone|ios", "iphone"),
("ipad", "ipad"),
(r"darwin|mac|os\s*x", "macos"),
("win", "windows"),
(r"android", "android"),
("netbsd", "netbsd"),
("openbsd", "openbsd"),
("freebsd", "freebsd"),
("dragonfly", "dragonflybsd"),
("(sun|i86)os", "solaris"),
(r"x11|lin(\b|ux)?", "linux"),
(r"nintendo\s+wii", "wii"),
("irix", "irix"),
("hp-?ux", "hpux"),
("aix", "aix"),
("sco|unix_sv", "sco"),
("bsd", "bsd"),
("amiga", "amiga"),
("blackberry|playbook", "blackberry"),
("symbian", "symbian"),
)
browsers = (
("googlebot", "google"),
("msnbot", "msn"),
("yahoo", "yahoo"),
("ask jeeves", "ask"),
(r"aol|america\s+online\s+browser", "aol"),
("opera", "opera"),
("edge", "edge"),
("chrome|crios", "chrome"),
("seamonkey", "seamonkey"),
("firefox|firebird|phoenix|iceweasel", "firefox"),
("galeon", "galeon"),
("safari|version", "safari"),
("webkit", "webkit"),
("camino", "camino"),
("konqueror", "konqueror"),
("k-meleon", "kmeleon"),
("netscape", "netscape"),
(r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"),
("lynx", "lynx"),
("links", "links"),
("Baiduspider", "baidu"),
("bingbot", "bing"),
("mozilla", "mozilla"),
)
_browser_version_re = r"(?:%s)[/\sa-z(]*(\d+[.\da-z]+)?"
_language_re = re.compile(
r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|"
r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)"
)
def __init__(self):
self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platforms]
self.browsers = [
(b, re.compile(self._browser_version_re % a, re.I))
for a, b in self.browsers
]
def __call__(self, user_agent):
for platform, regex in self.platforms: # noqa: B007
match = regex.search(user_agent)
if match is not None:
break
else:
platform = None
for browser, regex in self.browsers: # noqa: B007
match = regex.search(user_agent)
if match is not None:
version = match.group(1)
break
else:
browser = version = None
match = self._language_re.search(user_agent)
if match is not None:
language = match.group(1) or match.group(2)
else:
language = None
return platform, browser, version, language
class UserAgent(object):
"""Represents a user agent. Pass it a WSGI environment or a user agent
string and you can inspect some of the details from the user agent
string via the attributes. The following attributes exist:
.. attribute:: string
the raw user agent string
.. attribute:: platform
the browser platform. The following platforms are currently
recognized:
- `aix`
- `amiga`
- `android`
- `blackberry`
- `bsd`
- `chromeos`
- `dragonflybsd`
- `freebsd`
- `hpux`
- `ipad`
- `iphone`
- `irix`
- `linux`
- `macos`
- `netbsd`
- `openbsd`
- `sco`
- `solaris`
- `symbian`
- `wii`
- `windows`
.. attribute:: browser
the name of the browser. The following browsers are currently
recognized:
- `aol` *
- `ask` *
- `baidu` *
- `bing` *
- `camino`
- `chrome`
- `edge`
- `firefox`
- `galeon`
- `google` *
- `kmeleon`
- `konqueror`
- `links`
- `lynx`
- `mozilla`
- `msie`
- `msn`
- `netscape`
- `opera`
- `safari`
- `seamonkey`
- `webkit`
- `yahoo` *
(Browsers marked with a star (``*``) are crawlers.)
.. attribute:: version
the version of the browser
.. attribute:: language
the language of the browser
"""
_parser = UserAgentParser()
def __init__(self, environ_or_string):
if isinstance(environ_or_string, dict):
environ_or_string = environ_or_string.get("HTTP_USER_AGENT", "")
self.string = environ_or_string
self.platform, self.browser, self.version, self.language = self._parser(
environ_or_string
)
def to_header(self):
return self.string
def __str__(self):
return self.string
def __nonzero__(self):
return bool(self.browser)
__bool__ = __nonzero__
def __repr__(self):
return "<%s %r/%s>" % (self.__class__.__name__, self.browser, self.version)
+1 -1
View File
@@ -201,7 +201,7 @@ def log_ormcache_stats(sig=None, frame=None):
from odoo.modules.registry import Registry
import threading
me = threading.currentThread()
me = threading.current_thread()
me_dbname = getattr(me, 'dbname', 'n/a')
for dbname, reg in sorted(Registry.registries.d.items()):
+1 -1
View File
@@ -177,5 +177,5 @@ def randdatetime(*, base_date=None, relative_before=None, relative_after=None, s
seconds_after = relative_after and ((base_date + relative_after) - base_date).total_seconds() or 0
def get_rand_datetime(random=None, **kwargs):
return base_date + relativedelta(seconds=random.randint(seconds_before, seconds_after))
return base_date + relativedelta(seconds=random.randint(int(seconds_before), int(seconds_after)))
return compute(get_rand_datetime, seed=seed)
+2
View File
@@ -86,6 +86,8 @@ _EXPR_OPCODES = _CONST_OPCODES.union(to_opcodes([
# specialised comparisons
'IS_OP', 'CONTAINS_OP',
'DICT_MERGE', 'DICT_UPDATE',
# Basically used in any "generator literal"
'GEN_START', # added in 3.10 but already removed from 3.11.
])) - _BLACKLIST
_SAFE_OPCODES = _EXPR_OPCODES.union(to_opcodes([
+1 -1
View File
@@ -361,7 +361,7 @@ class GettextAlias(object):
def _get_db(self):
# find current DB based on thread/worker db name (see netsvc)
db_name = getattr(threading.currentThread(), 'dbname', None)
db_name = getattr(threading.current_thread(), 'dbname', None)
if db_name:
return odoo.sql_db.db_connect(db_name)
+8 -4
View File
@@ -6,16 +6,19 @@ ebaysdk==2.1.5
freezegun==0.3.11; python_version < '3.8'
freezegun==0.3.15; python_version >= '3.8'
gevent==1.5.0 ; python_version == '3.7'
gevent==20.9.0 ; python_version >= '3.8'
gevent==20.9.0 ; python_version > '3.7' and python_version <= '3.9'
gevent==21.8.0 ; python_version > '3.9' # (Jammy)
greenlet==0.4.15 ; python_version == '3.7'
greenlet==0.4.17 ; python_version > '3.7'
greenlet==0.4.17 ; python_version > '3.7' and python_version <= '3.9'
greenlet==1.1.2 ; python_version > '3.9' # (Jammy)
idna==2.8
Jinja2==2.11.3 # min version = 2.10.1 (Focal - with security backports)
libsass==0.18.0
lxml==4.6.5 # min version = 4.5.0 (Focal - with security backports)
MarkupSafe==1.1.0
num2words==0.5.6
ofxparse==0.19
ofxparse==0.19; python_version <= '3.9'
ofxparse==0.21; python_version > '3.9' # (Jammy)
passlib==1.7.3 # min version = 1.7.2 (Focal with security backports)
Pillow==9.0.1 # min version = 7.0.0 (Focal with security backports)
polib==1.1.0
@@ -37,7 +40,8 @@ reportlab==3.5.59 # version < 3.5.54 are not compatible with Pillow 8.1.2 and 3.
requests==2.25.1 # versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal)
urllib3==1.26.5 # indirect / min version = 1.25.8 (Focal with security backports)
vobject==0.9.6.1
Werkzeug==0.16.1
Werkzeug==0.16.1 ; python_version <= '3.9'
Werkzeug==2.0.2 ; python_version > '3.9' # (Jammy)
xlrd==1.1.0; python_version < '3.8'
xlrd==1.2.0; python_version >= '3.8'
XlsxWriter==1.1.2