Merge pull request #224

feat: add demo mode and allow for automatic admin creation from env variables
This commit is contained in:
Herculino Trotta
2025-03-31 02:29:44 -03:00
committed by GitHub
20 changed files with 381 additions and 52 deletions
+5
View File
@@ -10,6 +10,11 @@ SECRET_KEY=<GENERATE A SAFE SECRET KEY AND PLACE IT HERE>
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
OUTBOUND_PORT=9005 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=<ENTER YOUR EMAIL>
#ADMIN_PASSWORD=<YOUR SAFE PASSWORD>
SQL_DATABASE=wygiwyh SQL_DATABASE=wygiwyh
SQL_USER=wygiwyh SQL_USER=wygiwyh
SQL_PASSWORD=<INSERT A SAFE PASSWORD HERE> SQL_PASSWORD=<INSERT A SAFE PASSWORD HERE>
+4 -1
View File
@@ -76,7 +76,7 @@ $ nano .env # or any other editor you want to use
# Run the app # Run the app
$ docker compose up -d $ 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 $ docker compose exec -it web python manage.py createsuperuser
``` ```
@@ -129,6 +129,9 @@ 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. | | 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. | | 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 | | 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. |
# How it works # How it works
+5 -1
View File
@@ -261,7 +261,10 @@ if DEBUG:
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.DjangoModelPermissions"], "DEFAULT_PERMISSION_CLASSES": [
"apps.api.permissions.NotInDemoMode",
"rest_framework.permissions.DjangoModelPermissions",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10, "PAGE_SIZE": 10,
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
@@ -394,3 +397,4 @@ PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true" ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365")) KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
APP_VERSION = os.getenv("APP_VERSION", "unknown") APP_VERSION = os.getenv("APP_VERSION", "unknown")
DEMO = os.getenv("DEMO_MODE", "false").lower() == "true"
+10
View File
@@ -0,0 +1,10 @@
from rest_framework.permissions import BasePermission
from django.conf import settings
class NotInDemoMode(BasePermission):
def has_permission(self, request, view):
if settings.DEMO and not request.user.is_superuser:
return False
else:
return True
+15
View File
@@ -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
@@ -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."))
+38
View File
@@ -1,7 +1,9 @@
import logging import logging
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.conf import settings
from django.core import management from django.core import management
from django.db import DEFAULT_DB_ALIAS
from procrastinate import builtin_tasks from procrastinate import builtin_tasks
from procrastinate.contrib.django import app from procrastinate.contrib.django import app
@@ -40,3 +42,39 @@ async def remove_expired_sessions(timestamp=None):
"Error while executing 'remove_expired_sessions' task", "Error while executing 'remove_expired_sessions' task",
exc_info=True, 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
@@ -11,9 +11,11 @@ from apps.common.decorators.htmx import only_htmx
from apps.currencies.forms import ExchangeRateForm, ExchangeRateServiceForm from apps.currencies.forms import ExchangeRateForm, ExchangeRateServiceForm
from apps.currencies.models import ExchangeRate, ExchangeRateService from apps.currencies.models import ExchangeRate, ExchangeRateService
from apps.currencies.tasks import manual_fetch_exchange_rates from apps.currencies.tasks import manual_fetch_exchange_rates
from apps.common.decorators.demo import disabled_on_demo
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def exchange_rates_services_index(request): def exchange_rates_services_index(request):
return render( return render(
@@ -24,6 +26,7 @@ def exchange_rates_services_index(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def exchange_rates_services_list(request): def exchange_rates_services_list(request):
services = ExchangeRateService.objects.all() services = ExchangeRateService.objects.all()
@@ -37,6 +40,7 @@ def exchange_rates_services_list(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def exchange_rate_service_add(request): def exchange_rate_service_add(request):
if request.method == "POST": if request.method == "POST":
@@ -63,6 +67,7 @@ def exchange_rate_service_add(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def exchange_rate_service_edit(request, pk): def exchange_rate_service_edit(request, pk):
service = get_object_or_404(ExchangeRateService, id=pk) service = get_object_or_404(ExchangeRateService, id=pk)
@@ -91,6 +96,7 @@ def exchange_rate_service_edit(request, pk):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def exchange_rate_service_delete(request, pk): def exchange_rate_service_delete(request, pk):
service = get_object_or_404(ExchangeRateService, id=pk) service = get_object_or_404(ExchangeRateService, id=pk)
@@ -109,6 +115,7 @@ def exchange_rate_service_delete(request, pk):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def exchange_rate_service_force_fetch(request): def exchange_rate_service_force_fetch(request):
manual_fetch_exchange_rates.defer() manual_fetch_exchange_rates.defer()
+4
View File
@@ -41,11 +41,13 @@ from apps.export_app.resources.transactions import (
RecurringTransactionResource, RecurringTransactionResource,
) )
from apps.export_app.resources.users import UserResource from apps.export_app.resources.users import UserResource
from apps.common.decorators.demo import disabled_on_demo
logger = logging.getLogger() logger = logging.getLogger()
@login_required @login_required
@disabled_on_demo
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def export_index(request): def export_index(request):
@@ -53,6 +55,7 @@ def export_index(request):
@login_required @login_required
@disabled_on_demo
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def export_form(request): def export_form(request):
@@ -182,6 +185,7 @@ def export_form(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_form(request): def import_form(request):
+11
View File
@@ -13,9 +13,11 @@ from apps.import_app.forms import ImportRunFileUploadForm, ImportProfileForm
from apps.import_app.models import ImportRun, ImportProfile from apps.import_app.models import ImportRun, ImportProfile
from apps.import_app.services import PresetService from apps.import_app.services import PresetService
from apps.import_app.tasks import process_import from apps.import_app.tasks import process_import
from apps.common.decorators.demo import disabled_on_demo
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def import_presets_list(request): def import_presets_list(request):
presets = PresetService.get_all_presets() presets = PresetService.get_all_presets()
@@ -27,6 +29,7 @@ def import_presets_list(request):
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_profile_index(request): def import_profile_index(request):
return render( return render(
@@ -37,6 +40,7 @@ def import_profile_index(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_profile_list(request): def import_profile_list(request):
profiles = ImportProfile.objects.all() profiles = ImportProfile.objects.all()
@@ -50,6 +54,7 @@ def import_profile_list(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_profile_add(request): def import_profile_add(request):
message = request.POST.get("message", None) message = request.POST.get("message", None)
@@ -85,6 +90,7 @@ def import_profile_add(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_profile_edit(request, profile_id): def import_profile_edit(request, profile_id):
profile = get_object_or_404(ImportProfile, id=profile_id) profile = get_object_or_404(ImportProfile, id=profile_id)
@@ -114,6 +120,7 @@ def import_profile_edit(request, profile_id):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def import_profile_delete(request, profile_id): def import_profile_delete(request, profile_id):
profile = ImportProfile.objects.get(id=profile_id) profile = ImportProfile.objects.get(id=profile_id)
@@ -132,6 +139,7 @@ def import_profile_delete(request, profile_id):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_runs_list(request, profile_id): def import_runs_list(request, profile_id):
profile = ImportProfile.objects.get(id=profile_id) profile = ImportProfile.objects.get(id=profile_id)
@@ -147,6 +155,7 @@ def import_runs_list(request, profile_id):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_run_log(request, profile_id, run_id): def import_run_log(request, profile_id, run_id):
run = ImportRun.objects.get(profile__id=profile_id, 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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def import_run_add(request, profile_id): def import_run_add(request, profile_id):
profile = ImportProfile.objects.get(id=profile_id) profile = ImportProfile.objects.get(id=profile_id)
@@ -202,6 +212,7 @@ def import_run_add(request, profile_id):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def import_run_delete(request, profile_id, run_id): def import_run_delete(request, profile_id, run_id):
run = ImportRun.objects.get(profile__id=profile_id, id=run_id) run = ImportRun.objects.get(profile__id=profile_id, id=run_id)
+16
View File
@@ -18,9 +18,11 @@ from apps.rules.models import (
) )
from apps.common.models import SharedObject from apps.common.models import SharedObject
from apps.common.forms import SharedObjectForm from apps.common.forms import SharedObjectForm
from apps.common.decorators.demo import disabled_on_demo
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def rules_index(request): def rules_index(request):
return render( return render(
@@ -31,6 +33,7 @@ def rules_index(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def rules_list(request): def rules_list(request):
transaction_rules = TransactionRule.objects.all().order_by("id") transaction_rules = TransactionRule.objects.all().order_by("id")
@@ -43,6 +46,7 @@ def rules_list(request):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs): def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs):
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) 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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_add(request, **kwargs): def transaction_rule_add(request, **kwargs):
if request.method == "POST": if request.method == "POST":
@@ -91,6 +96,7 @@ def transaction_rule_add(request, **kwargs):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_edit(request, transaction_rule_id): def transaction_rule_edit(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_view(request, transaction_rule_id): def transaction_rule_view(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def transaction_rule_delete(request, transaction_rule_id): def transaction_rule_delete(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def transaction_rule_take_ownership(request, transaction_rule_id): def transaction_rule_take_ownership(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_share(request, pk): def transaction_rule_share(request, pk):
obj = get_object_or_404(TransactionRule, id=pk) obj = get_object_or_404(TransactionRule, id=pk)
@@ -225,6 +235,7 @@ def transaction_rule_share(request, pk):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_action_add(request, transaction_rule_id): def transaction_rule_action_add(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def transaction_rule_action_edit(request, transaction_rule_action_id): def transaction_rule_action_edit(request, transaction_rule_action_id):
transaction_rule_action = get_object_or_404( transaction_rule_action = get_object_or_404(
@@ -289,6 +301,7 @@ def transaction_rule_action_edit(request, transaction_rule_action_id):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def transaction_rule_action_delete(request, transaction_rule_action_id): def transaction_rule_action_delete(request, transaction_rule_action_id):
transaction_rule_action = get_object_or_404( transaction_rule_action = get_object_or_404(
@@ -309,6 +322,7 @@ def transaction_rule_action_delete(request, transaction_rule_action_id):
@only_htmx @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def update_or_create_transaction_rule_action_add(request, transaction_rule_id): def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def update_or_create_transaction_rule_action_edit(request, pk): def update_or_create_transaction_rule_action_edit(request, pk):
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=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 @only_htmx
@login_required @login_required
@disabled_on_demo
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def update_or_create_transaction_rule_action_delete(request, pk): def update_or_create_transaction_rule_action_delete(request, pk):
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk) linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
+34
View File
@@ -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"
}
}
]
@@ -5,6 +5,8 @@
<i class="fa-solid fa-user"></i> <i class="fa-solid fa-user"></i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-start dropdown-menu-lg-end"> <ul class="dropdown-menu dropdown-menu-start dropdown-menu-lg-end">
<li class="dropdown-item-text">{{ user.email }}</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" <li><a class="dropdown-item"
hx-get="{% url 'user_settings' %}" hx-get="{% url 'user_settings' %}"
hx-target="#generic-offcanvas" hx-target="#generic-offcanvas"
@@ -1,15 +1,30 @@
{% load i18n %} {% load i18n %}
<script type="text/hyperscript"> <script type="text/hyperscript">
behavior htmx_error_handler behavior htmx_error_handler
on htmx:responseError or htmx:afterRequest[detail.failed] or htmx:sendError queue none on htmx:responseError or htmx:afterRequest[detail.failed] or htmx:sendError queue none
call Swal.fire({title: '{% trans 'Something went wrong loading your data' %}', -- Check if the event detail contains the xhr object and the status is 403
text: '{% trans 'Try reloading the page or check the console for more information.' %}', if event.detail.xhr.status == 403 then
call Swal.fire({
title: '{% trans "Access Denied" %}',
text: '{% trans "You do not have permission to perform this action or access this resource." %}',
icon: 'warning',
customClass: {
confirmButton: 'btn btn-warning' -- Optional: different button style
},
buttonsStyling: true
})
else
call Swal.fire({
title: '{% trans "Something went wrong loading your data" %}',
text: '{% trans "Try reloading the page or check the console for more information." %}',
icon: 'error', icon: 'error',
customClass: { customClass: {
confirmButton: 'btn btn-primary' confirmButton: 'btn btn-primary'
}, },
buttonsStyling: true}) buttonsStyling: true
})
end
then log event then log event
then halt the event then halt the event
end end
</script> </script>
+19 -8
View File
@@ -1,3 +1,4 @@
{% load settings %}
{% load pwa %} {% load pwa %}
{% load formats %} {% load formats %}
{% load i18n %} {% load i18n %}
@@ -5,7 +6,7 @@
{% load webpack_loader %} {% load webpack_loader %}
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title> <title>
@@ -24,24 +25,34 @@
{% include 'includes/scripts.html' %} {% include 'includes/scripts.html' %}
{% block extra_js_head %}{% endblock %} {% block extra_js_head %}{% endblock %}
</head> </head>
<body class="font-monospace"> <body class="font-monospace">
<div _="install hide_amounts <div _="install hide_amounts
install htmx_error_handler install htmx_error_handler
{% block body_hyperscript %}{% endblock %}" {% block body_hyperscript %}{% endblock %}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
{% include 'includes/navbar.html' %} {% include 'includes/navbar.html' %}
{% settings "DEMO" as demo_mode %}
{% if demo_mode %}
<div class="px-3 m-0" id="demo-mode-alert" hx-preserve>
<div class="alert alert-warning alert-dismissible fade show my-3" role="alert">
<strong>{% trans 'This is a demo!' %}</strong> {% trans 'Any data you add here will be wiped in 24hrs or less' %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
<div id="content"> <div id="content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
{% include 'includes/offcanvas.html' %} {% include 'includes/offcanvas.html' %}
{% include 'includes/toasts.html' %} {% include 'includes/toasts.html' %}
</div> </div>
{% include 'includes/tools/calculator.html' %} {% include 'includes/tools/calculator.html' %}
{% block extra_js_body %}{% endblock %} {% block extra_js_body %}{% endblock %}
</body> </body>
</html> </html>
+14 -1
View File
@@ -1,4 +1,6 @@
{% extends "layouts/base_auth.html" %} {% extends "layouts/base_auth.html" %}
{% load i18n %}
{% load settings %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title %}Login{% endblock %} {% block title %}Login{% endblock %}
@@ -8,9 +10,20 @@
<div class="container"> <div class="container">
<div class="row vh-100 d-flex justify-content-center align-items-center"> <div class="row vh-100 d-flex justify-content-center align-items-center">
<div class="col-md-6 col-xl-4 col-12"> <div class="col-md-6 col-xl-4 col-12">
{% settings "DEMO" as demo_mode %}
{% if demo_mode %}
<div class="card shadow mb-3">
<div class="card-body">
<h1 class="h5 card-title text-center mb-4">{% trans "Welcome to WYGIWYH's demo!" %}</h1>
<p>{% trans 'Use the credentials below to login' %}:</p>
<p>{% trans 'E-mail' %}: <span class="badge text-bg-secondary user-select-all">demo@demo.com</span></p>
<p>{% trans 'Password' %}: <span class="badge text-bg-secondary user-select-all">wygiwyhdemo</span></p>
</div>
</div>
{% endif %}
<div class="card shadow-lg"> <div class="card shadow-lg">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-center mb-4">Login</h2> <h1 class="h2 card-title text-center mb-4">Login</h1>
{% crispy form %} {% crispy form %}
</div> </div>
</div> </div>
+2
View File
@@ -11,4 +11,6 @@ python manage.py migrate
# Create flag file to signal migrations are complete # Create flag file to signal migrations are complete
touch /tmp/migrations_complete touch /tmp/migrations_complete
python manage.py setup_users
exec python manage.py runserver 0.0.0.0:8000 exec python manage.py runserver 0.0.0.0:8000
+2
View File
@@ -13,4 +13,6 @@ python manage.py migrate
# Create flag file to signal migrations are complete # Create flag file to signal migrations are complete
touch /tmp/migrations_complete touch /tmp/migrations_complete
python manage.py setup_users
exec gunicorn WYGIWYH.wsgi:application --bind 0.0.0.0:8000 --timeout 600 exec gunicorn WYGIWYH.wsgi:application --bind 0.0.0.0:8000 --timeout 600