From c7ff6db0bf8945dd2886e8678ad44fbe11eb8f49 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Fri, 26 Dec 2025 09:55:57 -0300 Subject: [PATCH 1/2] feat(app): add sanity checks for env variables --- app/apps/common/apps.py | 3 + app/apps/common/checks.py | 103 ++++++++++++++++++ .../tests/test_import_service_v1.py | 1 - 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 app/apps/common/checks.py diff --git a/app/apps/common/apps.py b/app/apps/common/apps.py index 213972d..e934f44 100644 --- a/app/apps/common/apps.py +++ b/app/apps/common/apps.py @@ -23,3 +23,6 @@ class CommonConfig(AppConfig): # Delete the cache for update checks to prevent false-positives when the app is restarted # this will be recreated by the check_for_updates task cache.delete("update_check") + + # Register system checks for required environment variables + from apps.common import checks # noqa: F401 diff --git a/app/apps/common/checks.py b/app/apps/common/checks.py new file mode 100644 index 0000000..253176f --- /dev/null +++ b/app/apps/common/checks.py @@ -0,0 +1,103 @@ +""" +Django System Checks for required environment variables. + +This module validates that required environment variables (those without defaults) +are present before the application starts. +""" + +import os + +from django.core.checks import Error, register + + +# List of environment variables that are required (no default values) +# Based on the README.md documentation +REQUIRED_ENV_VARS = [ + ("SECRET_KEY", "This is used to provide cryptographic signing."), + ("SQL_DATABASE", "The name of your postgres database."), +] + +# List of environment variables that must be valid integers if set +INT_ENV_VARS = [ + ("TASK_WORKERS", "How many workers to have for async tasks."), + ("SESSION_EXPIRY_TIME", "The age of session cookies, in seconds."), + ("INTERNAL_PORT", "The port on which the app listens on."), + ("DJANGO_VITE_DEV_SERVER_PORT", "The port where Vite's dev server is running"), +] + + +@register() +def check_required_env_vars(app_configs, **kwargs): + """ + Check that all required environment variables are set. + + Returns a list of Error objects for any missing required variables. + """ + errors = [] + + for var_name, description in REQUIRED_ENV_VARS: + value = os.getenv(var_name) + if not value: + errors.append( + Error( + f"Required environment variable '{var_name}' is not set.", + hint=f"{description} Please set this variable in your .env file or environment.", + id="wygiwyh.E001", + ) + ) + + return errors + + +@register() +def check_int_env_vars(app_configs, **kwargs): + """ + Check that environment variables that should be integers are valid. + + Returns a list of Error objects for any invalid integer variables. + """ + errors = [] + + for var_name, description in INT_ENV_VARS: + value = os.getenv(var_name) + if value is not None: + try: + int(value) + except ValueError: + errors.append( + Error( + f"Environment variable '{var_name}' must be a valid integer, got '{value}'.", + hint=f"{description}", + id="wygiwyh.E002", + ) + ) + + return errors + + +@register() +def check_soft_delete_config(app_configs, **kwargs): + """ + Check that KEEP_DELETED_TRANSACTIONS_FOR is a valid integer when ENABLE_SOFT_DELETE is enabled. + + Returns a list of Error objects if the configuration is invalid. + """ + errors = [] + + enable_soft_delete = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true" + + if enable_soft_delete: + keep_deleted_for = os.getenv("KEEP_DELETED_TRANSACTIONS_FOR") + if keep_deleted_for is not None: + try: + int(keep_deleted_for) + except ValueError: + errors.append( + Error( + f"Environment variable 'KEEP_DELETED_TRANSACTIONS_FOR' must be a valid integer when ENABLE_SOFT_DELETE is enabled, got '{keep_deleted_for}'.", + hint="Time in days to keep soft deleted transactions for. Set to 0 to keep all transactions indefinitely.", + id="wygiwyh.E003", + ) + ) + + return errors diff --git a/app/apps/import_app/tests/test_import_service_v1.py b/app/apps/import_app/tests/test_import_service_v1.py index 44f7e5c..47d3ab5 100644 --- a/app/apps/import_app/tests/test_import_service_v1.py +++ b/app/apps/import_app/tests/test_import_service_v1.py @@ -8,7 +8,6 @@ is only used for string fields (not dates, decimals, etc.). from datetime import date from decimal import Decimal -from unittest.mock import MagicMock, patch from django.test import TestCase From 2076903740254484d74a0f0ec40e0d32ed061992 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sat, 27 Dec 2025 23:43:57 -0300 Subject: [PATCH 2/2] refactor: order management lists by name instead of id --- app/apps/accounts/views/account_groups.py | 2 +- app/apps/accounts/views/accounts.py | 2 +- app/apps/currencies/views/currencies.py | 2 +- app/apps/dca/views.py | 2 +- app/apps/transactions/views/categories.py | 4 ++-- app/apps/transactions/views/entities.py | 4 ++-- app/apps/transactions/views/tags.py | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/apps/accounts/views/account_groups.py b/app/apps/accounts/views/account_groups.py index 8d524dd..d9b0825 100644 --- a/app/apps/accounts/views/account_groups.py +++ b/app/apps/accounts/views/account_groups.py @@ -25,7 +25,7 @@ def account_groups_index(request): @login_required @require_http_methods(["GET"]) def account_groups_list(request): - account_groups = AccountGroup.objects.all().order_by("id") + account_groups = AccountGroup.objects.all().order_by("name") return render( request, "account_groups/fragments/list.html", diff --git a/app/apps/accounts/views/accounts.py b/app/apps/accounts/views/accounts.py index d45c746..4908bb7 100644 --- a/app/apps/accounts/views/accounts.py +++ b/app/apps/accounts/views/accounts.py @@ -25,7 +25,7 @@ def accounts_index(request): @login_required @require_http_methods(["GET"]) def accounts_list(request): - accounts = Account.objects.all().order_by("id") + accounts = Account.objects.all().order_by("name") return render( request, "accounts/fragments/list.html", diff --git a/app/apps/currencies/views/currencies.py b/app/apps/currencies/views/currencies.py index 1fa431a..7962f91 100644 --- a/app/apps/currencies/views/currencies.py +++ b/app/apps/currencies/views/currencies.py @@ -23,7 +23,7 @@ def currencies_index(request): @login_required @require_http_methods(["GET"]) def currencies_list(request): - currencies = Currency.objects.all().order_by("id") + currencies = Currency.objects.all().order_by("name") return render( request, "currencies/fragments/list.html", diff --git a/app/apps/dca/views.py b/app/apps/dca/views.py index d2892b1..2bf3f38 100644 --- a/app/apps/dca/views.py +++ b/app/apps/dca/views.py @@ -23,7 +23,7 @@ def strategy_index(request): @only_htmx @login_required def strategy_list(request): - strategies = DCAStrategy.objects.all().order_by("created_at") + strategies = DCAStrategy.objects.all().order_by("name") return render( request, "dca/fragments/strategy/list.html", {"strategies": strategies} ) diff --git a/app/apps/transactions/views/categories.py b/app/apps/transactions/views/categories.py index 5293be3..43bd5ff 100644 --- a/app/apps/transactions/views/categories.py +++ b/app/apps/transactions/views/categories.py @@ -35,7 +35,7 @@ def categories_list(request): @login_required @require_http_methods(["GET"]) def categories_table_active(request): - categories = TransactionCategory.objects.filter(active=True).order_by("id") + categories = TransactionCategory.objects.filter(active=True).order_by("name") return render( request, "categories/fragments/table.html", @@ -47,7 +47,7 @@ def categories_table_active(request): @login_required @require_http_methods(["GET"]) def categories_table_archived(request): - categories = TransactionCategory.objects.filter(active=False).order_by("id") + categories = TransactionCategory.objects.filter(active=False).order_by("name") return render( request, "categories/fragments/table.html", diff --git a/app/apps/transactions/views/entities.py b/app/apps/transactions/views/entities.py index cd46e4b..300d3e2 100644 --- a/app/apps/transactions/views/entities.py +++ b/app/apps/transactions/views/entities.py @@ -35,7 +35,7 @@ def entities_list(request): @login_required @require_http_methods(["GET"]) def entities_table_active(request): - entities = TransactionEntity.objects.filter(active=True).order_by("id") + entities = TransactionEntity.objects.filter(active=True).order_by("name") return render( request, "entities/fragments/table.html", @@ -47,7 +47,7 @@ def entities_table_active(request): @login_required @require_http_methods(["GET"]) def entities_table_archived(request): - entities = TransactionEntity.objects.filter(active=False).order_by("id") + entities = TransactionEntity.objects.filter(active=False).order_by("name") return render( request, "entities/fragments/table.html", diff --git a/app/apps/transactions/views/tags.py b/app/apps/transactions/views/tags.py index a79babc..f7709c9 100644 --- a/app/apps/transactions/views/tags.py +++ b/app/apps/transactions/views/tags.py @@ -35,7 +35,7 @@ def tags_list(request): @login_required @require_http_methods(["GET"]) def tags_table_active(request): - tags = TransactionTag.objects.filter(active=True).order_by("id") + tags = TransactionTag.objects.filter(active=True).order_by("name") return render( request, "tags/fragments/table.html", @@ -47,7 +47,7 @@ def tags_table_active(request): @login_required @require_http_methods(["GET"]) def tags_table_archived(request): - tags = TransactionTag.objects.filter(active=False).order_by("id") + tags = TransactionTag.objects.filter(active=False).order_by("name") return render( request, "tags/fragments/table.html",