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:

Reply via email to