From 84c47a0433859f672545cacdc86d5d8e5c1b514b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 12 Nov 2023 23:44:43 +0100 Subject: [PATCH] Support mounting application under a prefix Closes #9239 --- .../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(-) diff --git a/modules/marketing_automation/marketing_automation.py b/modules/marketing_automation/marketing_automation.py index a8ae786bf9..14b45b15b7 100644 --- a/modules/marketing_automation/marketing_automation.py +++ b/modules/marketing_automation/marketing_automation.py @@ -30,7 +30,7 @@ from trytond.tools import pairwise_longest 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 @@ class Activity( 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 --git a/modules/marketing_email/marketing.py b/modules/marketing_email/marketing.py index 7b65f9ac92..8a2ed93b23 100644 --- a/modules/marketing_email/marketing.py +++ b/modules/marketing_email/marketing.py @@ -26,7 +26,7 @@ from trytond.tools.email_ import ( 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 @@ class Message(Workflow, ModelSQL, ModelView): 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 --git a/modules/product_image/product.py b/modules/product_image/product.py index 87cad7ea25..004bdba345 100644 --- a/modules/product_image/product.py +++ b/modules/product_image/product.py @@ -16,7 +16,7 @@ from trytond.pool import PoolMeta 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 @@ class ImageURLMixin: 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 --git a/modules/web_shortener/web.py b/modules/web_shortener/web.py index 71f2405644..8211c29937 100644 --- a/modules/web_shortener/web.py +++ b/modules/web_shortener/web.py @@ -12,7 +12,7 @@ import trytond.config as config 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 @@ class ShortenedURL(ModelSQL, ModelView): '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 --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 5d7f71efe0..27b35c31f1 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -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 --git a/trytond/doc/topics/configuration.rst b/trytond/doc/topics/configuration.rst index 5e1eec3b12..f3076613cd 100644 --- a/trytond/doc/topics/configuration.rst +++ b/trytond/doc/topics/configuration.rst @@ -66,6 +66,16 @@ hostname 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 --git a/trytond/trytond/config.py b/trytond/trytond/config.py index 3ff5bcfba7..d8347f8c94 100644 --- a/trytond/trytond/config.py +++ b/trytond/trytond/config.py @@ -55,6 +55,7 @@ class TrytonConfigParser(configparser.ConfigParser): 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 --git a/trytond/trytond/ir/avatar.py b/trytond/trytond/ir/avatar.py index 9be29ad9e3..785aca5121 100644 --- a/trytond/trytond/ir/avatar.py +++ b/trytond/trytond/ir/avatar.py @@ -90,7 +90,7 @@ class Avatar(ImageMixin, ResourceMixin, ModelSQL): 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 --git a/trytond/trytond/protocols/wrappers.py b/trytond/trytond/protocols/wrappers.py index 5fd2eb5dfb..861e79c18c 100644 --- a/trytond/trytond/protocols/wrappers.py +++ b/trytond/trytond/protocols/wrappers.py @@ -200,6 +200,7 @@ class Request(_Request): 'http_host': self.environ.get('HTTP_HOST'), 'scheme': self.scheme, 'is_secure': self.is_secure, + 'root_path': self.root_path, } diff --git a/trytond/trytond/tests/test_res.py b/trytond/trytond/tests/test_res.py index 70ececa945..80c462f416 100644 --- a/trytond/trytond/tests/test_res.py +++ b/trytond/trytond/tests/test_res.py @@ -35,7 +35,7 @@ class ResTestCase(ModuleTestCase): 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 --git a/trytond/trytond/url.py b/trytond/trytond/url.py index b4e86ac1b0..974a8d7a5e 100644 --- a/trytond/trytond/url.py +++ b/trytond/trytond/url.py @@ -14,6 +14,7 @@ HOSTNAME = (config.get('web', 'hostname') 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,12 +49,31 @@ class URLAccessor(object): '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': return 'http' + ('s' if self.is_secure() else '') 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': @@ -82,13 +102,15 @@ class URLAccessor(object): '%(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: