details: https://code.tryton.org/tryton/commit/a90668af7ef9
branch: default
user: Cédric Krier <[email protected]>
date: Sun Nov 12 23:44:43 2023 +0100
description:
Support mounting application under a prefix
Closes #9239
diffstat:
modules/marketing_automation/marketing_automation.py | 4 +-
modules/marketing_email/marketing.py | 4 +-
modules/product_image/product.py | 4 +-
modules/web_shortener/web.py | 4 +-
trytond/CHANGELOG | 1 +
trytond/doc/topics/configuration.rst | 10 +++++++
trytond/trytond/config.py | 1 +
trytond/trytond/ir/avatar.py | 2 +-
trytond/trytond/protocols/wrappers.py | 1 +
trytond/trytond/tests/test_res.py | 2 +-
trytond/trytond/url.py | 26 ++++++++++++++++++-
11 files changed, 47 insertions(+), 12 deletions(-)
diffs (220 lines):
diff -r ab430e9809f1 -r a90668af7ef9
modules/marketing_automation/marketing_automation.py
--- a/modules/marketing_automation/marketing_automation.py Thu Apr 02
15:32:01 2026 +0200
+++ b/modules/marketing_automation/marketing_automation.py Sun Nov 12
23:44:43 2023 +0100
@@ -30,7 +30,7 @@
from trytond.tools.chart import sparkline
from trytond.tools.email_ import format_address, has_rcpt, set_from_header
from trytond.transaction import Transaction
-from trytond.url import http_host
+from trytond.url import http_base
from trytond.wsgi import Base64Converter
from .exceptions import ConditionError, DomainError, TemplateError
@@ -636,7 +636,7 @@
Email = pool.get('ir.email')
record = record_activity.record
url_base = config.get(
- 'marketing', 'automation_base', default=http_host())
+ 'marketing', 'automation_base', default=http_base())
url_open = urljoin(url_base, '/m/empty.gif')
with Transaction().set_context(language=record.language):
diff -r ab430e9809f1 -r a90668af7ef9 modules/marketing_email/marketing.py
--- a/modules/marketing_email/marketing.py Thu Apr 02 15:32:01 2026 +0200
+++ b/modules/marketing_email/marketing.py Sun Nov 12 23:44:43 2023 +0100
@@ -26,7 +26,7 @@
EmailNotValidError, format_address, normalize_email, set_from_header,
validate_email)
from trytond.transaction import Transaction, inactive_records
-from trytond.url import http_host
+from trytond.url import http_base
from trytond.wizard import Button, StateTransition, StateView, Wizard
from .exceptions import EMailValidationError, TemplateError
@@ -430,7 +430,7 @@
spy_pixel = config.getboolean(
'marketing', 'email_spy_pixel', default=False)
- url_base = config.get('marketing', 'email_base', default=http_host())
+ url_base = config.get('marketing', 'email_base', default=http_base())
url_open = urljoin(url_base, '/m/empty.gif')
@lru_cache(None)
diff -r ab430e9809f1 -r a90668af7ef9 modules/product_image/product.py
--- a/modules/product_image/product.py Thu Apr 02 15:32:01 2026 +0200
+++ b/modules/product_image/product.py Sun Nov 12 23:44:43 2023 +0100
@@ -16,7 +16,7 @@
from trytond.pyson import Bool, Eval, If
from trytond.tools import slugify
from trytond.transaction import Transaction
-from trytond.url import http_host
+from trytond.url import http_base
from trytond.wsgi import Base64Converter
from .exceptions import ImageValidationError
@@ -77,7 +77,7 @@
url_base = config.get(
'product', 'image_base', default='')
url_external_base = config.get(
- 'product', 'image_base', default=http_host())
+ 'product', 'image_base', default=http_base())
return self._image_url(
url_external_base if _external else url_base, **args)
diff -r ab430e9809f1 -r a90668af7ef9 modules/web_shortener/web.py
--- a/modules/web_shortener/web.py Thu Apr 02 15:32:01 2026 +0200
+++ b/modules/web_shortener/web.py Sun Nov 12 23:44:43 2023 +0100
@@ -12,7 +12,7 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
-from trytond.url import http_host
+from trytond.url import http_base
from trytond.wsgi import Base64Converter
ALPHABET = string.digits + string.ascii_lowercase
@@ -37,7 +37,7 @@
'database': Base64Converter(None).to_url(
Transaction().database.name),
}
- url_base = config.get('web', 'shortener_base', default=http_host())
+ url_base = config.get('web', 'shortener_base', default=http_base())
for shortened in shortened_urls:
url_parts['short_id'] = cls._shorten(shortened.id)
urls[shortened.id] = urljoin(
diff -r ab430e9809f1 -r a90668af7ef9 trytond/CHANGELOG
--- a/trytond/CHANGELOG Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/CHANGELOG Sun Nov 12 23:44:43 2023 +0100
@@ -1,3 +1,4 @@
+* Support mounting application under a prefix
* Send emails on chat messages
* Add path attribute on XML fields to specfiy a relative path value
* Allow to include subdirectories in tryton.cfg
diff -r ab430e9809f1 -r a90668af7ef9 trytond/doc/topics/configuration.rst
--- a/trytond/doc/topics/configuration.rst Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/doc/topics/configuration.rst Sun Nov 12 23:44:43 2023 +0100
@@ -66,6 +66,16 @@
Defines the hostname to use when generating a URL when there is no request
context available, for example during a cron job.
+.. _config-web.root_path:
+
+root_path
+~~~~~~~~~
+
+Defines the prefix that the WSGI application is mounted under (with trailing
slash).
+This must be the same as the ``SCRIPT_NAME`` set by the WSGI server.
+
+The default value is: ``/``.
+
.. _config-web.root:
root
diff -r ab430e9809f1 -r a90668af7ef9 trytond/trytond/config.py
--- a/trytond/trytond/config.py Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/trytond/config.py Sun Nov 12 23:44:43 2023 +0100
@@ -55,6 +55,7 @@
super().__init__(interpolation=None)
self.add_section('web')
self.set('web', 'listen', 'localhost:8000')
+ self.set('web', 'root_path', '/')
self.set('web', 'root', os.path.join(os.path.expanduser('~'), 'www'))
self.set('web', 'num_proxies', '0')
self.set('web', 'cache_timeout', str(60 * 60 * 12))
diff -r ab430e9809f1 -r a90668af7ef9 trytond/trytond/ir/avatar.py
--- a/trytond/trytond/ir/avatar.py Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/trytond/ir/avatar.py Sun Nov 12 23:44:43 2023 +0100
@@ -90,7 +90,7 @@
if self.image_id or self.image:
url_base = config.get('web', 'avatar_base', default='')
return urljoin(
- url_base, quote('/avatar/%(database)s/%(uuid)s' % {
+ url_base, quote('avatar/%(database)s/%(uuid)s' % {
'database': Base64Converter(None).to_url(
Transaction().database.name),
'uuid': self.uuid,
diff -r ab430e9809f1 -r a90668af7ef9 trytond/trytond/protocols/wrappers.py
--- a/trytond/trytond/protocols/wrappers.py Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/trytond/protocols/wrappers.py Sun Nov 12 23:44:43 2023 +0100
@@ -200,6 +200,7 @@
'http_host': self.environ.get('HTTP_HOST'),
'scheme': self.scheme,
'is_secure': self.is_secure,
+ 'root_path': self.root_path,
}
diff -r ab430e9809f1 -r a90668af7ef9 trytond/trytond/tests/test_res.py
--- a/trytond/trytond/tests/test_res.py Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/trytond/tests/test_res.py Sun Nov 12 23:44:43 2023 +0100
@@ -35,7 +35,7 @@
self.assertEqual(len(user.avatars), 1)
self.assertIsNotNone(user.avatar)
- self.assertRegex(user.avatar_url, r'/avatar/.*/([0-9a-fA-F]{12})')
+ self.assertRegex(user.avatar_url, r'avatar/.*/([0-9a-fA-F]{12})')
@with_transaction()
def test_user_warning(self):
diff -r ab430e9809f1 -r a90668af7ef9 trytond/trytond/url.py
--- a/trytond/trytond/url.py Thu Apr 02 15:32:01 2026 +0200
+++ b/trytond/trytond/url.py Sun Nov 12 23:44:43 2023 +0100
@@ -14,6 +14,7 @@
or socket.getfqdn())
HOSTNAME = '.'.join(encodings.idna.ToASCII(part).decode('ascii')
if part else '' for part in HOSTNAME.split('.'))
+ROOT_PATH = config.get('web', 'root_path')
class URLAccessor(object):
@@ -48,6 +49,19 @@
'http' + ('s' if cls.is_secure() else ''),
cls.host(), '', '', ''))
+ @classmethod
+ def http_root_path(cls):
+ context = Transaction().context
+ if context:
+ request = context.get('_request')
+ if request:
+ return request['root_path']
+ return ROOT_PATH
+
+ @classmethod
+ def http_base(cls):
+ return urllib.parse.urljoin(cls.http_host(), cls.http_root_path())
+
@property
def protocol(self):
if self._protocol == 'http':
@@ -55,6 +69,12 @@
return self._protocol
@property
+ def root_path(self):
+ if self._protocol == 'http':
+ return self.http_root_path()
+ return '/'
+
+ @property
def separator(self):
if self._protocol == 'http':
return '#'
@@ -82,13 +102,15 @@
'%(database)s/%(type)s/%(name)s' % url_part)
if isinstance(inst, Model) and inst.id:
local_part += '/%d' % inst.id
- return '%s://%s/%s%s' % (
- self.protocol, self.host(), self.separator, local_part)
+ return (
+ f'{self.protocol}://{self.host()}{self.root_path}'
+ f'{self.separator}{local_part}')
is_secure = URLAccessor.is_secure
host = URLAccessor.host
http_host = URLAccessor.http_host
+http_base = URLAccessor.http_base
class URLMixin: