Repository: dynaconf/dynaconf (Python)

Issue:
[bug] Django `fix_absolute_urls` does not fix `STATIC_URL` script prefix
**Describe the bug**
This issue is related to the previously closed issue #1098. The bug still persists because the post hook does not run during the settings loading process.

**To Reproduce**
Steps to reproduce the behavior:

1. Having the following folder structure

First, initialize a Django project using:
```
django-admin startproject project .
```

Then, add Dynaconf to settings.py using:
```
dynaconf init --django project.settings.py
```

<details>
<summary> Project structure </summary>

```bash
.
├── config.py
├── manage.py
├── project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── pyproject.toml
├── settings.toml
└── uv.lock

```
</details>

2. Having the following config files:

Using django default settings.

3. Having the following app code:

<details>
<summary> Code </summary>

**project/settings.py**
(at very bottom of `settings.py`, add by `dynaconf init` command)

```python
...

# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
# Read more at https://www.dynaconf.com/django/
import dynaconf  # noqa
settings = dynaconf.DjangoDynaconf(__name__)  # noqa
# HERE ENDS DYNACONF EXTENSION LOAD (No more code below this line)
    
```

</details>

4. Executing under the following environment

<details>
<summary> Execution </summary>

```bash
fix_absolute_urls 1 % uv run python manage.py shell                                                                                                                                                                                                                             18:25:17
6 objects imported automatically (use -v 2 for details).

Python 3.11.5 (main, Aug 26 2023, 08:35:59) [Clang 16.0.3 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.conf import settings
>>> settings.STATIC_URL
'static/'
>>> settings.reload()
>>> settings.STATIC_URL
'/static/'
```

</details>

**Expected behavior**
The `STATIC_URL` setting should automatically include the leading slash (/static/) without requiring a reload.

**Environment (please complete the following information):**
 - OS: Mac 14.6.1
 - Dynaconf Version: 3.3.0.dev0
 - Frameworks in use: Django latest

**Additional context**
Upon inspecting the source code, it seems that the line:

https://github.com/dynaconf/dynaconf/blob/32f38478350a80020e7aab60a3cbd29dd7f8d9ea/dynaconf/base.py#L1281

is executed before the line:

https://github.com/dynaconf/dynaconf/blob/32f38478350a80020e7aab60a3cbd29dd7f8d9ea/dynaconf/contrib/django_dynaconf_v2.py#L137



Discussion:

Patch:
diff --git a/dynaconf/contrib/django_dynaconf_v2.py b/dynaconf/contrib/django_dynaconf_v2.py
index 7dee1523a..5d0235171 100644
--- a/dynaconf/contrib/django_dynaconf_v2.py
+++ b/dynaconf/contrib/django_dynaconf_v2.py
@@ -134,6 +134,10 @@ class UserSettingsHolder(dynaconf.LazySettings):
 
     lazy_settings.update(dj)
 
+    # 2 pass execution, 1st immediately and second deferred
+    # to post hooks so it can fix URLS added later in the
+    # loading pipeline
+    lazy_settings.update(fix_absolute_urls(lazy_settings))
     lazy_settings._post_hooks.append(fix_absolute_urls)
 
     # Allow dynaconf_hooks to be in the same folder as the django.settings


Test patch:
diff --git a/tests_functional/issues/1307_django_static_url_fix/.gitignore b/tests_functional/issues/1307_django_static_url_fix/.gitignore
new file mode 100644
index 000000000..bf035d263
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/.gitignore
@@ -0,0 +1,3 @@
+
+# Ignore dynaconf secret files
+.secrets.*
diff --git a/tests_functional/issues/1307_django_static_url_fix/Makefile b/tests_functional/issues/1307_django_static_url_fix/Makefile
new file mode 100644
index 000000000..1877debb6
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/Makefile
@@ -0,0 +1,4 @@
+.PHONY: test
+
+test:
+	PYTHONPATH=. DJANGO_SETTINGS_MODULE=project.settings django-admin shell -c "from django.conf import settings;settings.STATIC_URL == '/static/' or exit(1)"
diff --git a/tests_functional/issues/1307_django_static_url_fix/config.py b/tests_functional/issues/1307_django_static_url_fix/config.py
new file mode 100644
index 000000000..2bc0d724f
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/config.py
@@ -0,0 +1,9 @@
+from dynaconf import Dynaconf
+
+settings = Dynaconf(
+    envvar_prefix="DYNACONF",
+    settings_files=["settings.toml", ".secrets.toml"],
+)
+
+# `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`.
+# `settings_files` = Load these files in the order.
diff --git a/tests_functional/issues/1307_django_static_url_fix/manage.py b/tests_functional/issues/1307_django_static_url_fix/manage.py
new file mode 100755
index 000000000..317280df2
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests_functional/issues/1307_django_static_url_fix/project/__init__.py b/tests_functional/issues/1307_django_static_url_fix/project/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests_functional/issues/1307_django_static_url_fix/project/asgi.py b/tests_functional/issues/1307_django_static_url_fix/project/asgi.py
new file mode 100644
index 000000000..bd9acbce6
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
+
+application = get_asgi_application()
diff --git a/tests_functional/issues/1307_django_static_url_fix/project/settings.py b/tests_functional/issues/1307_django_static_url_fix/project/settings.py
new file mode 100644
index 000000000..7b0e6a6da
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/project/settings.py
@@ -0,0 +1,129 @@
+"""
+Django settings for project project.
+
+Generated by 'django-admin startproject' using Django 5.2.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.2/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "project.urls"
+
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = "project.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+DATABASES = {
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": BASE_DIR / "db.sqlite3",
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = "static/"
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+
+# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
+# Read more at https://www.dynaconf.com/django/
+import dynaconf  # noqa
+
+settings = dynaconf.DjangoDynaconf(__name__)  # noqa
+# HERE ENDS DYNACONF EXTENSION LOAD (No more code below this line)
diff --git a/tests_functional/issues/1307_django_static_url_fix/project/urls.py b/tests_functional/issues/1307_django_static_url_fix/project/urls.py
new file mode 100644
index 000000000..bac607327
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/project/urls.py
@@ -0,0 +1,23 @@
+"""
+URL configuration for project project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/5.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+    path("admin/", admin.site.urls),
+]
diff --git a/tests_functional/issues/1307_django_static_url_fix/project/wsgi.py b/tests_functional/issues/1307_django_static_url_fix/project/wsgi.py
new file mode 100644
index 000000000..0936bdcbc
--- /dev/null
+++ b/tests_functional/issues/1307_django_static_url_fix/project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
+
+application = get_wsgi_application()
diff --git a/tests_functional/issues/1307_django_static_url_fix/settings.toml b/tests_functional/issues/1307_django_static_url_fix/settings.toml
new file mode 100644
index 000000000..e69de29bb