From 046e02d5067653c9aa33215bf046b37cf48606ec Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Mon, 31 Mar 2025 02:11:13 -0300 Subject: [PATCH 1/4] feat(app): add environment variables to automatically create superuser on startup --- .env.example | 5 + README.md | 4 +- .../common/management/commands/setup_users.py | 137 ++++++++++++++++++ docker/dev/django/start | 2 + docker/prod/django/start | 2 + 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 app/apps/common/management/commands/setup_users.py diff --git a/.env.example b/.env.example index dfe49c0..4199907 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,11 @@ SECRET_KEY= DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] OUTBOUND_PORT=9005 +# Uncomment these variables to automatically create an admin account using these credentials on startup. +# After your first successfull login you can remove these variables from your file for safety reasons. +#ADMIN_EMAIL= +#ADMIN_PASSWORD= + SQL_DATABASE=wygiwyh SQL_USER=wygiwyh SQL_PASSWORD= diff --git a/README.md b/README.md index 60fcceb..05733d6 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ $ nano .env # or any other editor you want to use # Run the app $ docker compose up -d -# Create the first admin account +# Create the first admin account. This isn't required if you set the enviroment variables: ADMIN_EMAIL and ADMIN_PASSWORD. $ docker compose exec -it web python manage.py createsuperuser ``` @@ -129,6 +129,8 @@ To create the first user, open the container's console using Unraid's UI, by cli | ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. | | KEEP_DELETED_TRANSACTIONS_FOR | int | 365 | Time in days to keep soft deleted transactions for. If 0, will keep all transactions indefinitely. Only works if ENABLE_SOFT_DELETE is true. | | TASK_WORKERS | int | 1 | How many workers to have for async tasks. One should be enough for most use cases | +| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. | +| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. | # How it works diff --git a/app/apps/common/management/commands/setup_users.py b/app/apps/common/management/commands/setup_users.py new file mode 100644 index 0000000..f8abcca --- /dev/null +++ b/app/apps/common/management/commands/setup_users.py @@ -0,0 +1,137 @@ +import os +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth import get_user_model +from django.conf import settings +from django.db import IntegrityError + +# Get the custom User model if defined, otherwise the default User model +User = get_user_model() + + +class Command(BaseCommand): + help = ( + "Creates a superuser from environment variables (ADMIN_EMAIL, ADMIN_PASSWORD) " + "and optionally creates a demo user (demo@demo.com) if settings.DEMO is True." + ) + + def handle(self, *args, **options): + self.stdout.write("Starting user setup...") + + # --- Create Superuser --- + admin_email = os.environ.get("ADMIN_EMAIL") + admin_password = os.environ.get("ADMIN_PASSWORD") + + if admin_email and admin_password: + self.stdout.write(f"Attempting to create superuser: {admin_email}") + # Use email as username for simplicity, requires USERNAME_FIELD='email' + # or adapt if your USERNAME_FIELD is different. + # If USERNAME_FIELD is 'username', you might need ADMIN_USERNAME env var. + username_field = User.USERNAME_FIELD # Get the actual username field name + + # Check if the user already exists by email or username + user_exists_kwargs = {"email": admin_email} + if username_field != "email": + # Assume username should also be the email if not explicitly provided + user_exists_kwargs[username_field] = admin_email + + if User.objects.filter(**user_exists_kwargs).exists(): + self.stdout.write( + self.style.WARNING( + f"Superuser with email '{admin_email}' (or corresponding username) already exists. Skipping creation." + ) + ) + else: + try: + create_kwargs = { + username_field: admin_email, # Use email as username by default + "email": admin_email, + "password": admin_password, + } + User.objects.create_superuser(**create_kwargs) + self.stdout.write( + self.style.SUCCESS( + f"Superuser '{admin_email}' created successfully." + ) + ) + except IntegrityError as e: + self.stdout.write( + self.style.ERROR( + f"Failed to create superuser '{admin_email}'. IntegrityError: {e}" + ) + ) + except Exception as e: + self.stdout.write( + self.style.ERROR( + f"An unexpected error occurred creating superuser '{admin_email}': {e}" + ) + ) + else: + self.stdout.write( + self.style.NOTICE( + "ADMIN_EMAIL or ADMIN_PASSWORD environment variables not set. Skipping superuser creation." + ) + ) + + self.stdout.write("---") # Separator + + # --- Create Demo User --- + # Use getattr to safely check for the DEMO setting, default to False if not present + create_demo_user = getattr(settings, "DEMO", False) + + if create_demo_user: + demo_email = "demo@demo.com" + demo_password = ( + "wygiwyhdemo" # Consider making this an env var too for security + ) + demo_username = demo_email # Using email as username for consistency + + self.stdout.write( + f"DEMO setting is True. Attempting to create demo user: {demo_email}" + ) + + username_field = User.USERNAME_FIELD # Get the actual username field name + + # Check if the user already exists by email or username + user_exists_kwargs = {"email": demo_email} + if username_field != "email": + user_exists_kwargs[username_field] = demo_username + + if User.objects.filter(**user_exists_kwargs).exists(): + self.stdout.write( + self.style.WARNING( + f"Demo user with email '{demo_email}' (or corresponding username) already exists. Skipping creation." + ) + ) + else: + try: + create_kwargs = { + username_field: demo_username, + "email": demo_email, + "password": demo_password, + } + User.objects.create_user(**create_kwargs) + self.stdout.write( + self.style.SUCCESS( + f"Demo user '{demo_email}' created successfully." + ) + ) + except IntegrityError as e: + self.stdout.write( + self.style.ERROR( + f"Failed to create demo user '{demo_email}'. IntegrityError: {e}" + ) + ) + except Exception as e: + self.stdout.write( + self.style.ERROR( + f"An unexpected error occurred creating demo user '{demo_email}': {e}" + ) + ) + else: + self.stdout.write( + self.style.NOTICE( + "DEMO setting is not True (or not set). Skipping demo user creation." + ) + ) + + self.stdout.write(self.style.SUCCESS("User setup command finished.")) diff --git a/docker/dev/django/start b/docker/dev/django/start index dded5aa..8fe851c 100644 --- a/docker/dev/django/start +++ b/docker/dev/django/start @@ -11,4 +11,6 @@ python manage.py migrate # Create flag file to signal migrations are complete touch /tmp/migrations_complete +python manage.py setup_users + exec python manage.py runserver 0.0.0.0:8000 diff --git a/docker/prod/django/start b/docker/prod/django/start index f581619..ffb47a8 100644 --- a/docker/prod/django/start +++ b/docker/prod/django/start @@ -13,4 +13,6 @@ python manage.py migrate # Create flag file to signal migrations are complete touch /tmp/migrations_complete +python manage.py setup_users + exec gunicorn WYGIWYH.wsgi:application --bind 0.0.0.0:8000 --timeout 600 From 47d34f3c27522919a89a7b78e0dc8bb36ea37528 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Mon, 31 Mar 2025 02:14:00 -0300 Subject: [PATCH 2/4] feat(app): add a demo mode --- README.md | 3 +- app/WYGIWYH/settings.py | 1 + app/apps/common/decorators/demo.py | 15 ++++ app/apps/common/management/__init__.py | 0 .../common/management/commands/__init__.py | 0 app/apps/common/tasks.py | 38 ++++++++++ .../views/exchange_rates_services.py | 7 ++ app/apps/export_app/views.py | 4 ++ app/apps/import_app/views.py | 11 +++ app/apps/rules/views.py | 16 +++++ app/fixtures/demo_data.json | 34 +++++++++ .../hyperscript/htmx_error_handler.html | 39 ++++++---- app/templates/layouts/base.html | 71 +++++++++++-------- app/templates/users/login.html | 27 +++++-- 14 files changed, 216 insertions(+), 50 deletions(-) create mode 100644 app/apps/common/decorators/demo.py create mode 100644 app/apps/common/management/__init__.py create mode 100644 app/apps/common/management/commands/__init__.py create mode 100644 app/fixtures/demo_data.json diff --git a/README.md b/README.md index 05733d6..ca580db 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ To create the first user, open the container's console using Unraid's UI, by cli |-------------------------------|-------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | DJANGO_ALLOWED_HOSTS | string | localhost 127.0.0.1 | A list of space separated domains and IPs representing the host/domain names that WYGIWYH site can serve. [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#allowed-hosts) for more details | | HTTPS_ENABLED | true\|false | false | Whether to use secure cookies. If this is set to true, the cookie will be marked as “secure”, which means browsers may ensure that the cookie is only sent under an HTTPS connection | -| URL | string | http://localhost http://127.0.0.1 | A list of space separated domains and IPs (with the protocol) representing the trusted origins for unsafe requests (e.g. POST). [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#csrf-trusted-origins ) for more details | +| URL | string | http://localhost http://127.0.0.1 | A list of space separated domains and IPs (with the protocol) representing the trusted origins for unsafe requests (e.g. POST). [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#csrf-trusted-origins ) for more details | | SECRET_KEY | string | "" | This is used to provide cryptographic signing, and should be set to a unique, unpredictable value. | | DEBUG | true\|false | false | Turns DEBUG mode on or off, this is useful to gather more data about possible errors you're having. Don't use in production. | | SQL_DATABASE | string | None *required | The name of your postgres database | @@ -129,6 +129,7 @@ To create the first user, open the container's console using Unraid's UI, by cli | ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. | | KEEP_DELETED_TRANSACTIONS_FOR | int | 365 | Time in days to keep soft deleted transactions for. If 0, will keep all transactions indefinitely. Only works if ENABLE_SOFT_DELETE is true. | | TASK_WORKERS | int | 1 | How many workers to have for async tasks. One should be enough for most use cases | +| DEMO | true\|false | false | If demo mode is enabled. | | ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. | | ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. | diff --git a/app/WYGIWYH/settings.py b/app/WYGIWYH/settings.py index 009233f..63c142d 100644 --- a/app/WYGIWYH/settings.py +++ b/app/WYGIWYH/settings.py @@ -394,3 +394,4 @@ PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js" ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true" KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365")) APP_VERSION = os.getenv("APP_VERSION", "unknown") +DEMO = os.getenv("DEMO_MODE", "false").lower() == "true" diff --git a/app/apps/common/decorators/demo.py b/app/apps/common/decorators/demo.py new file mode 100644 index 0000000..933d8b4 --- /dev/null +++ b/app/apps/common/decorators/demo.py @@ -0,0 +1,15 @@ +from functools import wraps + +from django.conf import settings +from django.core.exceptions import PermissionDenied + + +def disabled_on_demo(view): + @wraps(view) + def _view(request, *args, **kwargs): + if settings.DEMO and not request.user.is_superuser: + raise PermissionDenied + + return view(request, *args, **kwargs) + + return _view diff --git a/app/apps/common/management/__init__.py b/app/apps/common/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/apps/common/management/commands/__init__.py b/app/apps/common/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/apps/common/tasks.py b/app/apps/common/tasks.py index 0a8d688..e9c33b4 100644 --- a/app/apps/common/tasks.py +++ b/app/apps/common/tasks.py @@ -1,7 +1,9 @@ import logging from asgiref.sync import sync_to_async +from django.conf import settings from django.core import management +from django.db import DEFAULT_DB_ALIAS from procrastinate import builtin_tasks from procrastinate.contrib.django import app @@ -40,3 +42,39 @@ async def remove_expired_sessions(timestamp=None): "Error while executing 'remove_expired_sessions' task", exc_info=True, ) + + +@app.periodic(cron="0 6 * * *") +@app.task(name="reset_demo_data") +def reset_demo_data(): + """ + Wipes the database and loads fresh demo data if DEMO mode is active. + Runs daily at 6:00 AM. + """ + if not settings.DEMO: + return # Exit if not in demo mode + + logger.info("Demo mode active. Starting daily data reset...") + + try: + # 1. Flush the database (wipe all data) + logger.info("Flushing the database...") + # Using --noinput prevents prompts. Specify database if not default. + management.call_command( + "flush", "--noinput", database=DEFAULT_DB_ALIAS, verbosity=1 + ) + logger.info("Database flushed successfully.") + + # 2. Load data from the fixture + fixture_name = "fixtures/demo_data.json" + logger.info(f"Loading data from fixture: {fixture_name}...") + management.call_command( + "loaddata", fixture_name, database=DEFAULT_DB_ALIAS, verbosity=1 + ) + logger.info(f"Data loaded successfully from {fixture_name}.") + + logger.info("Daily demo data reset completed.") + + except Exception as e: + logger.exception(f"Error during daily demo data reset: {e}") + raise diff --git a/app/apps/currencies/views/exchange_rates_services.py b/app/apps/currencies/views/exchange_rates_services.py index c572f3b..7548769 100644 --- a/app/apps/currencies/views/exchange_rates_services.py +++ b/app/apps/currencies/views/exchange_rates_services.py @@ -11,9 +11,11 @@ from apps.common.decorators.htmx import only_htmx from apps.currencies.forms import ExchangeRateForm, ExchangeRateServiceForm from apps.currencies.models import ExchangeRate, ExchangeRateService from apps.currencies.tasks import manual_fetch_exchange_rates +from apps.common.decorators.demo import disabled_on_demo @login_required +@disabled_on_demo @require_http_methods(["GET"]) def exchange_rates_services_index(request): return render( @@ -24,6 +26,7 @@ def exchange_rates_services_index(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET"]) def exchange_rates_services_list(request): services = ExchangeRateService.objects.all() @@ -37,6 +40,7 @@ def exchange_rates_services_list(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def exchange_rate_service_add(request): if request.method == "POST": @@ -63,6 +67,7 @@ def exchange_rate_service_add(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def exchange_rate_service_edit(request, pk): service = get_object_or_404(ExchangeRateService, id=pk) @@ -91,6 +96,7 @@ def exchange_rate_service_edit(request, pk): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["DELETE"]) def exchange_rate_service_delete(request, pk): service = get_object_or_404(ExchangeRateService, id=pk) @@ -109,6 +115,7 @@ def exchange_rate_service_delete(request, pk): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET"]) def exchange_rate_service_force_fetch(request): manual_fetch_exchange_rates.defer() diff --git a/app/apps/export_app/views.py b/app/apps/export_app/views.py index 94dd7bc..b60f4a0 100644 --- a/app/apps/export_app/views.py +++ b/app/apps/export_app/views.py @@ -41,11 +41,13 @@ from apps.export_app.resources.transactions import ( RecurringTransactionResource, ) from apps.export_app.resources.users import UserResource +from apps.common.decorators.demo import disabled_on_demo logger = logging.getLogger() @login_required +@disabled_on_demo @user_passes_test(lambda u: u.is_superuser) @require_http_methods(["GET"]) def export_index(request): @@ -53,6 +55,7 @@ def export_index(request): @login_required +@disabled_on_demo @user_passes_test(lambda u: u.is_superuser) @require_http_methods(["GET", "POST"]) def export_form(request): @@ -182,6 +185,7 @@ def export_form(request): @only_htmx @login_required +@disabled_on_demo @user_passes_test(lambda u: u.is_superuser) @require_http_methods(["GET", "POST"]) def import_form(request): diff --git a/app/apps/import_app/views.py b/app/apps/import_app/views.py index a9f45ad..7309ecc 100644 --- a/app/apps/import_app/views.py +++ b/app/apps/import_app/views.py @@ -13,9 +13,11 @@ from apps.import_app.forms import ImportRunFileUploadForm, ImportProfileForm from apps.import_app.models import ImportRun, ImportProfile from apps.import_app.services import PresetService from apps.import_app.tasks import process_import +from apps.common.decorators.demo import disabled_on_demo @login_required +@disabled_on_demo @require_http_methods(["GET"]) def import_presets_list(request): presets = PresetService.get_all_presets() @@ -27,6 +29,7 @@ def import_presets_list(request): @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_profile_index(request): return render( @@ -37,6 +40,7 @@ def import_profile_index(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_profile_list(request): profiles = ImportProfile.objects.all() @@ -50,6 +54,7 @@ def import_profile_list(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_profile_add(request): message = request.POST.get("message", None) @@ -85,6 +90,7 @@ def import_profile_add(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_profile_edit(request, profile_id): profile = get_object_or_404(ImportProfile, id=profile_id) @@ -114,6 +120,7 @@ def import_profile_edit(request, profile_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["DELETE"]) def import_profile_delete(request, profile_id): profile = ImportProfile.objects.get(id=profile_id) @@ -132,6 +139,7 @@ def import_profile_delete(request, profile_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_runs_list(request, profile_id): profile = ImportProfile.objects.get(id=profile_id) @@ -147,6 +155,7 @@ def import_runs_list(request, profile_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_run_log(request, profile_id, run_id): run = ImportRun.objects.get(profile__id=profile_id, id=run_id) @@ -160,6 +169,7 @@ def import_run_log(request, profile_id, run_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def import_run_add(request, profile_id): profile = ImportProfile.objects.get(id=profile_id) @@ -202,6 +212,7 @@ def import_run_add(request, profile_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["DELETE"]) def import_run_delete(request, profile_id, run_id): run = ImportRun.objects.get(profile__id=profile_id, id=run_id) diff --git a/app/apps/rules/views.py b/app/apps/rules/views.py index 79e8588..7ac0013 100644 --- a/app/apps/rules/views.py +++ b/app/apps/rules/views.py @@ -18,9 +18,11 @@ from apps.rules.models import ( ) from apps.common.models import SharedObject from apps.common.forms import SharedObjectForm +from apps.common.decorators.demo import disabled_on_demo @login_required +@disabled_on_demo @require_http_methods(["GET"]) def rules_index(request): return render( @@ -31,6 +33,7 @@ def rules_index(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET"]) def rules_list(request): transaction_rules = TransactionRule.objects.all().order_by("id") @@ -43,6 +46,7 @@ def rules_list(request): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -65,6 +69,7 @@ def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_add(request, **kwargs): if request.method == "POST": @@ -91,6 +96,7 @@ def transaction_rule_add(request, **kwargs): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_edit(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -129,6 +135,7 @@ def transaction_rule_edit(request, transaction_rule_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_view(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -142,6 +149,7 @@ def transaction_rule_view(request, transaction_rule_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["DELETE"]) def transaction_rule_delete(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -166,6 +174,7 @@ def transaction_rule_delete(request, transaction_rule_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET"]) def transaction_rule_take_ownership(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -187,6 +196,7 @@ def transaction_rule_take_ownership(request, transaction_rule_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_share(request, pk): obj = get_object_or_404(TransactionRule, id=pk) @@ -225,6 +235,7 @@ def transaction_rule_share(request, pk): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_action_add(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -252,6 +263,7 @@ def transaction_rule_action_add(request, transaction_rule_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_action_edit(request, transaction_rule_action_id): transaction_rule_action = get_object_or_404( @@ -289,6 +301,7 @@ def transaction_rule_action_edit(request, transaction_rule_action_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["DELETE"]) def transaction_rule_action_delete(request, transaction_rule_action_id): transaction_rule_action = get_object_or_404( @@ -309,6 +322,7 @@ def transaction_rule_action_delete(request, transaction_rule_action_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def update_or_create_transaction_rule_action_add(request, transaction_rule_id): transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) @@ -340,6 +354,7 @@ def update_or_create_transaction_rule_action_add(request, transaction_rule_id): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["GET", "POST"]) def update_or_create_transaction_rule_action_edit(request, pk): linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk) @@ -374,6 +389,7 @@ def update_or_create_transaction_rule_action_edit(request, pk): @only_htmx @login_required +@disabled_on_demo @require_http_methods(["DELETE"]) def update_or_create_transaction_rule_action_delete(request, pk): linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk) diff --git a/app/fixtures/demo_data.json b/app/fixtures/demo_data.json new file mode 100644 index 0000000..e184873 --- /dev/null +++ b/app/fixtures/demo_data.json @@ -0,0 +1,34 @@ +[ +{ + "model": "users.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$870000$kUmqeqdenjc2yFS8qbniwS$7qOMXzKG+yFmezdjhptkwuMJlqlZnQHXgAnonWurpBk=", + "last_login": "2025-03-31T03:22:25Z", + "is_superuser": false, + "first_name": "Demo", + "last_name": "User", + "is_staff": false, + "is_active": true, + "date_joined": "2025-03-31T03:21:04Z", + "email": "demo@demo.com", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "users.usersettings", + "pk": 1, + "fields": { + "user": 1, + "hide_amounts": false, + "mute_sounds": false, + "date_format": "SHORT_DATE_FORMAT", + "datetime_format": "SHORT_DATETIME_FORMAT", + "number_format": "AA", + "language": "auto", + "timezone": "auto", + "start_page": "MONTHLY_OVERVIEW" + } +} +] diff --git a/app/templates/includes/scripts/hyperscript/htmx_error_handler.html b/app/templates/includes/scripts/hyperscript/htmx_error_handler.html index 9bff690..f6c6d8d 100644 --- a/app/templates/includes/scripts/hyperscript/htmx_error_handler.html +++ b/app/templates/includes/scripts/hyperscript/htmx_error_handler.html @@ -1,15 +1,30 @@ {% load i18n %} diff --git a/app/templates/layouts/base.html b/app/templates/layouts/base.html index 90f0384..d5dc92f 100644 --- a/app/templates/layouts/base.html +++ b/app/templates/layouts/base.html @@ -1,3 +1,4 @@ +{% load settings %} {% load pwa %} {% load formats %} {% load i18n %} @@ -5,43 +6,53 @@ {% load webpack_loader %} - - - - - {% filter site_title %} - {% block title %} - {% endblock title %} - {% endfilter %} - + + + + + {% filter site_title %} + {% block title %} + {% endblock title %} + {% endfilter %} + - {% include 'includes/head/favicons.html' %} - {% progressive_web_app_meta %} + {% include 'includes/head/favicons.html' %} + {% progressive_web_app_meta %} - {% include 'includes/styles.html' %} - {% block extra_styles %}{% endblock %} - - {% include 'includes/scripts.html' %} + {% include 'includes/styles.html' %} + {% block extra_styles %}{% endblock %} - {% block extra_js_head %}{% endblock %} - - -
+
- {% include 'includes/navbar.html' %} + hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> + {% include 'includes/navbar.html' %} -
- {% block content %}{% endblock %} -
- - {% include 'includes/offcanvas.html' %} - {% include 'includes/toasts.html' %} + {% settings "DEMO" as demo_mode %} + {% if demo_mode %} +
+ +
+ {% endif %} - {% include 'includes/tools/calculator.html' %} +
+ {% block content %}{% endblock %} +
- {% block extra_js_body %}{% endblock %} - + {% include 'includes/offcanvas.html' %} + {% include 'includes/toasts.html' %} +
+ +{% include 'includes/tools/calculator.html' %} + +{% block extra_js_body %}{% endblock %} + diff --git a/app/templates/users/login.html b/app/templates/users/login.html index df4a648..341884c 100644 --- a/app/templates/users/login.html +++ b/app/templates/users/login.html @@ -1,4 +1,6 @@ {% extends "layouts/base_auth.html" %} +{% load i18n %} +{% load settings %} {% load crispy_forms_tags %} {% block title %}Login{% endblock %} @@ -7,15 +9,26 @@
-
-
-
-

Login

- {% crispy form %} -
-
+
+ {% settings "DEMO" as demo_mode %} + {% if demo_mode %} +
+
+

{% trans "Welcome to WYGIWYH's demo!" %}

+

{% trans 'Use the credentials below to login' %}:

+

{% trans 'E-mail' %}: demo@demo.com

+

{% trans 'Password' %}: wygiwyhdemo

+
+ {% endif %} +
+
+

Login

+ {% crispy form %} +
+
+
{% endblock %} From 19f0bc1034618991c99c2bf753b028ff070f78c0 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Mon, 31 Mar 2025 02:28:33 -0300 Subject: [PATCH 3/4] feat(app): show current user e-mail on user menu --- app/templates/includes/navbar/user_menu.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/templates/includes/navbar/user_menu.html b/app/templates/includes/navbar/user_menu.html index 7c21eae..1232b3e 100644 --- a/app/templates/includes/navbar/user_menu.html +++ b/app/templates/includes/navbar/user_menu.html @@ -5,6 +5,8 @@