diff --git a/bbs/api/queries/users.py b/bbs/api/queries/users.py index 1ba0a07..8c435eb 100644 --- a/bbs/api/queries/users.py +++ b/bbs/api/queries/users.py @@ -1,5 +1,4 @@ import strawberry -import strawberry_django from django.contrib.auth.models import User as DjangoUser from strawberry import ID @@ -8,7 +7,9 @@ from bbs.api.types.users import User @strawberry.type class UsersQueries: - all_users: list[User] = strawberry_django.field() + @strawberry.field + def all_users(self) -> list[User]: + return DjangoUser.objects.filter(profile__isnull=False) @strawberry.field def user(self, user_id: ID) -> User: diff --git a/bbs/management/commands/seed_demo_data.py b/bbs/management/commands/seed_demo_data.py new file mode 100644 index 0000000..17956c1 --- /dev/null +++ b/bbs/management/commands/seed_demo_data.py @@ -0,0 +1,151 @@ +import os +from datetime import datetime, time, timedelta +from decimal import Decimal + +from django.contrib.auth import get_user_model +from django.core.management import BaseCommand +from django.db.models import Sum +from django.utils import timezone + +from bbs.models import InventoryTransaction, Order, OrderLine, Product, Transaction, UserProfile + +DEMO_USERS = ( + ("demo-ada", "Ada", "Nord", "ada@example.test", Decimal("300.0")), + ("demo-bjørn", "Bjørn", "Berg", "bjorn@example.test", Decimal("250.0")), + ("demo-casey", "Casey", "Sol", "casey@example.test", Decimal("200.0")), +) + +DEMO_PRODUCTS = ( + ("Demo Cola", Decimal("25.0"), Decimal("30.0"), 30), + ("Demo Kaffe", Decimal("15.0"), Decimal("20.0"), 30), + ("Demo Sjokolade", Decimal("20.0"), Decimal("25.0"), 30), + ("Demo Energidrikk", Decimal("35.0"), Decimal("40.0"), 30), +) + + +class Command(BaseCommand): + help = "Create repeatable demo users, products, deposits, and orders for local testing." + + def handle(self, *args, **options): # noqa: ARG002 + users = self.seed_users() + products = self.seed_products(users[0][0]) + self.seed_deposits(users) + self.seed_orders(users, products) + self.update_balances(users) + self.create_admin() + + self.stdout.write(self.style.SUCCESS("Demo data is ready.")) + if not os.environ.get("BBS_SEED_ADMIN_PASSWORD"): + self.stdout.write("Create an admin login with: uv run manage.py createsuperuser") + + @staticmethod + def seed_users(): + user_model = get_user_model() + users = [] + for username, first_name, last_name, email, deposit_amount in DEMO_USERS: + user, _ = user_model.objects.get_or_create( + username=username, + defaults={"first_name": first_name, "last_name": last_name, "email": email}, + ) + UserProfile.objects.get_or_create(user=user) + users.append((user, deposit_amount)) + return users + + @staticmethod + def seed_products(stock_user): + products = {} + for name, sale_price_int, sale_price_ext, stock in DEMO_PRODUCTS: + product, created = Product.objects.get_or_create( + name=name, + defaults={ + "sale_price_int": sale_price_int, + "sale_price_ext": sale_price_ext, + "inventory_amount": stock, + "description": "Demo product for local development.", + }, + ) + products[name] = product + if created: + InventoryTransaction.objects.get_or_create( + user=stock_user, + product=product, + comment="Demo opening stock", + defaults={"amount": stock, "unit_price": Decimal("10.0")}, + ) + return products + + def seed_deposits(self, users): + for index, (user, deposit_amount) in enumerate(users): + created_at = self.timestamp(days_ago=32 + index) + deposit, created = Transaction.objects.get_or_create( + user=user, + created=created_at, + defaults={"amount": deposit_amount}, + ) + if created: + deposit.created = created_at + deposit.save(update_fields=["created"]) + + def seed_orders(self, users, products): + orders = ( + (0, 24, (("Demo Cola", 2), ("Demo Sjokolade", 1))), + (1, 19, (("Demo Kaffe", 2), ("Demo Sjokolade", 1))), + (2, 11, (("Demo Energidrikk", 1),)), + (0, 4, (("Demo Kaffe", 1), ("Demo Cola", 1))), + (1, 1, (("Demo Cola", 2),)), + ) + for user_index, days_ago, lines in orders: + user = users[user_index][0] + created_at = self.timestamp(days_ago=days_ago) + total = sum( + (products[name].sale_price_int * amount for name, amount in lines), + start=Decimal("0"), + ) + order, created = Order.objects.get_or_create( + customer=user, + created=created_at, + defaults={"order_sum": total}, + ) + if created: + order.created = created_at + order.save(update_fields=["created"]) + for name, amount in lines: + product = products[name] + OrderLine.objects.create( + order=order, + product=product, + amount=amount, + unit_price=product.sale_price_int, + ) + product.inventory_amount -= amount + product.save(update_fields=["inventory_amount"]) + + @staticmethod + def update_balances(users): + for user, _ in users: + deposits = Transaction.objects.filter(user=user).aggregate(total=Sum("amount"))["total"] or Decimal("0") + purchases = Order.objects.filter(customer=user).aggregate(total=Sum("order_sum"))["total"] or Decimal("0") + profile = user.profile + profile.balance = deposits - purchases + profile.save(update_fields=["balance"]) + + def create_admin(self): + admin_password = os.environ.get("BBS_SEED_ADMIN_PASSWORD") + if not admin_password: + return + + user_model = get_user_model() + if user_model.objects.filter(username="admin").exists(): + self.stdout.write("Existing 'admin' account left unchanged.") + else: + user_model.objects.create_superuser( + username="admin", + email="admin@example.test", + password=admin_password, + ) + self.stdout.write("Created superuser 'admin'.") + + @staticmethod + def timestamp(days_ago: int) -> datetime: + date = timezone.localdate() - timedelta(days=days_ago) + return timezone.make_aware(datetime.combine(date, time(hour=12))) diff --git a/bbs/tests/test_seed_demo_data.py b/bbs/tests/test_seed_demo_data.py new file mode 100644 index 0000000..3eae2bd --- /dev/null +++ b/bbs/tests/test_seed_demo_data.py @@ -0,0 +1,42 @@ +import pytest # noqa: INP001 +from django.contrib.auth import get_user_model +from django.core.management import call_command +from django.test import Client +from strawberry.django.test import GraphQLTestClient + +from bbs.models import InventoryTransaction, Order, Product, Transaction + + +@pytest.mark.django_db +def test_seed_demo_data_is_repeatable(): + call_command("seed_demo_data") + call_command("seed_demo_data") + + assert get_user_model().objects.filter(username__startswith="demo-").count() == 3 + assert Product.objects.filter(name__startswith="Demo ").count() == 4 + assert Order.objects.count() == 5 + assert Transaction.objects.count() == 3 + assert InventoryTransaction.objects.count() == 4 + assert sum(product.inventory_amount for product in Product.objects.filter(name__startswith="Demo ")) == 109 + + +@pytest.mark.django_db +def test_demo_data_is_available_when_an_admin_has_no_profile(): + user_model = get_user_model() + user_model.objects.create_superuser(username="admin", email="admin@example.test", password="password") + call_command("seed_demo_data") + + gql_client = GraphQLTestClient(Client()) + result = gql_client.query( + """ + { + allProducts(filters: {active: true}) { name } + allUsers { firstName lastName profile { balance } } + } + """ + ) + + assert not result.errors + assert result.data + assert len(result.data["allProducts"]) == 4 + assert len(result.data["allUsers"]) == 3