mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-12 07:42:53 +02:00
Merge pull request #224
feat: add demo mode and allow for automatic admin creation from env variables
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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."))
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -2,13 +2,28 @@
|
|||||||
<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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% load settings %}
|
||||||
{% load pwa %}
|
{% load pwa %}
|
||||||
{% load formats %}
|
{% load formats %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
@@ -32,6 +33,16 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user