diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 6d6af87eaf..b0ff845010 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -17,6 +17,7 @@ from django.contrib.staticfiles.storage import StaticFilesStorage from django.core.exceptions import FieldError, ValidationError from django.core.files.storage import default_storage from django.core.validators import URLValidator +from django.db.utils import OperationalError, ProgrammingError from django.http import StreamingHttpResponse from django.test import TestCase from django.utils.translation import gettext_lazy as _ @@ -88,31 +89,57 @@ def getStaticUrl(filename): return os.path.join(STATIC_URL, str(filename)) -def construct_absolute_url(*arg): +def construct_absolute_url(*arg, **kwargs): """Construct (or attempt to construct) an absolute URL from a relative URL. This is useful when (for example) sending an email to a user with a link to something in the InvenTree web framework. - This requires the BASE_URL configuration option to be set! + A URL is constructed in the following order: + + 1. If setings.SITE_URL is set (e.g. in the Django settings), use that + 2. If the InvenTree setting INVENTREE_BASE_URL is set, use that + 3. Otherwise, use the current request URL (if available) """ - base = str(common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL')) - url = '/'.join(arg) + relative_url = '/'.join(arg) - if not base: - return url + # If a site URL is provided, use that + site_url = getattr(settings, 'SITE_URL', None) + + if not site_url: + # Otherwise, try to use the InvenTree setting + try: + site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False) + except ProgrammingError: + pass + except OperationalError: + pass + + if not site_url: + # Otherwise, try to use the current request + request = kwargs.get('request', None) + + if request: + site_url = request.build_absolute_uri('/') + + if not site_url: + # No site URL available, return the relative URL + return relative_url # Strip trailing slash from base url - if base.endswith('/'): - base = base[:-1] + if site_url.endswith('/'): + site_url = site_url[:-1] - if url.startswith('/'): - url = url[1:] + if relative_url.startswith('/'): + relative_url = relative_url[1:] - url = f"{base}/{url}" + return f"{site_url}/{relative_url}" - return url + +def get_base_url(**kwargs): + """Return the base URL for the InvenTree server""" + return construct_absolute_url('', **kwargs) def download_image_from_url(remote_url, timeout=2.5): diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f820c8aa2d..38a921c778 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -17,6 +17,7 @@ from pathlib import Path import django.conf.locale import django.core.exceptions +from django.core.validators import URLValidator from django.http import Http404 from django.utils.translation import gettext_lazy as _ @@ -908,6 +909,16 @@ PLUGIN_TESTING_EVENTS = False PLUGIN_RETRY = get_setting('INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 5) # How often should plugin loading be tried? PLUGIN_FILE_CHECKED = False # Was the plugin file checked? +# Site URL can be specified statically, or via a run-time setting +SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None) + +if SITE_URL: + logger.info(f"Site URL: {SITE_URL}") + + # Check that the site URL is valid + validator = URLValidator() + validator(SITE_URL) + # User interface customization values CUSTOM_LOGO = get_custom_file('INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True) CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', 'custom splash') diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index f771930e0b..284e672693 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -71,6 +71,10 @@ language: en-us # Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones timezone: UTC +# Base URL for the InvenTree server +# Use the environment variable INVENTREE_BASE_URL +# base_url: 'http://localhost:8000' + # Base currency code (or use env var INVENTREE_BASE_CURRENCY) base_currency: USD diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index 978aa8977b..f36cf70234 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -13,10 +13,9 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.translation import gettext_lazy as _ -import common.models import part.models import stock.models -from InvenTree.helpers import normalize, validateFilterString +from InvenTree.helpers import get_base_url, normalize, validateFilterString from InvenTree.models import MetadataMixin from plugin.registry import registry @@ -183,7 +182,7 @@ class LabelTemplate(MetadataMixin, models.Model): context = self.get_context_data(request) # Add "basic" context data which gets passed to every label - context['base_url'] = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL') + context['base_url'] = get_base_url(request=request) context['date'] = datetime.datetime.now().date() context['datetime'] = datetime.datetime.now() context['request'] = request diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 8b87cd26d8..00ac96e29b 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -3,7 +3,6 @@ from django.contrib.auth import authenticate, login from django.db import transaction from django.db.models import F, Q -from django.db.utils import ProgrammingError from django.http.response import JsonResponse from django.urls import include, path, re_path from django.utils.translation import gettext_lazy as _ @@ -20,7 +19,8 @@ from company.models import SupplierPart from InvenTree.api import (APIDownloadMixin, AttachmentMixin, ListCreateDestroyAPIView, MetadataView, StatusView) from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS -from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.helpers import (DownloadFile, construct_absolute_url, + get_base_url, str2bool) from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI) from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus, @@ -1371,11 +1371,8 @@ class OrderCalendarExport(ICalFeed): whether or not to show completed orders. Defaults to false """ - try: - instance_url = InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False) - except ProgrammingError: # pragma: no cover - # database is not initialized yet - instance_url = '' + instance_url = get_base_url() + instance_url = instance_url.replace("http://", "").replace("https://", "") timezone = settings.TIME_ZONE file_name = "calendar.ics" @@ -1507,9 +1504,7 @@ class OrderCalendarExport(ICalFeed): def item_link(self, item): """Set the item link.""" - # Do not use instance_url as here, as the protocol needs to be included - site_url = InvenTreeSetting.get_setting("INVENTREE_BASE_URL") - return f'{site_url}{item.get_absolute_url()}' + return construct_absolute_url(item.get_absolute_url()) order_api_urls = [ diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 19e4cfe1bb..d264dff2e9 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -222,8 +222,8 @@ def inventree_splash(**kwargs): @register.simple_tag() def inventree_base_url(*args, **kwargs): - """Return the INVENTREE_BASE_URL setting.""" - return InvenTreeSetting.get_setting('INVENTREE_BASE_URL') + """Return the base URL of the InvenTree server""" + return InvenTree.helpers.get_base_url() @register.simple_tag() diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 25127b6747..3f6e482570 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -20,7 +20,7 @@ import common.models import order.models import part.models import stock.models -from InvenTree.helpers import validateFilterString +from InvenTree.helpers import get_base_url, validateFilterString from InvenTree.models import MetadataMixin from plugin.registry import registry @@ -203,7 +203,7 @@ class ReportTemplateBase(MetadataMixin, ReportBase): # Generate custom context data based on the particular report subclass context = self.get_context_data(request) - context['base_url'] = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL') + context['base_url'] = get_base_url(request=request) context['date'] = datetime.datetime.now().date() context['datetime'] = datetime.datetime.now() context['default_page_size'] = common.models.InvenTreeSetting.get_setting('REPORT_DEFAULT_PAGE_SIZE') diff --git a/InvenTree/report/templatetags/report.py b/InvenTree/report/templatetags/report.py index c7f3365c04..e617264f8e 100644 --- a/InvenTree/report/templatetags/report.py +++ b/InvenTree/report/templatetags/report.py @@ -205,10 +205,7 @@ def logo_image(**kwargs): def internal_link(link, text): """Make a href which points to an InvenTree URL. - Important Note: This only works if the INVENTREE_BASE_URL parameter is set! - - If the INVENTREE_BASE_URL parameter is not configured, - the text will be returned (unlinked) + Uses the InvenTree.helpers.construct_absolute_url function to build the URL. """ text = str(text) diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index 08700acdeb..2dd071df87 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -56,6 +56,15 @@ The following basic options are available: | INVENTREE_TIMZONE | timezome | Server timezone | UTC | | ADMIN_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin | | INVENTREE_LANGUAGE | language | Default language | en-us | +| INVENTREE_BASE_URL | base_url | Server base URL | *Not specified* | + +### Base URL Configuration + +The base URL of the InvenTree site is required for constructing absolute URLs in a number of circumstances. To construct a URL, the InvenTree iterates through the following options in decreasing order of importance: + +1. Static configuration (i.e. set using environment variable or configuration file as above) +2. Global settings (i.e. configured at run-time in the [global settings](../settings/global.md)) +3. Using the hostname supplied by the user request ## Administrator Account