Add Norwegian demo data, avatars and a random mode to seed_demo_data
This commit is contained in:
3 files changed
+297
-30
No files matched your search
@@ -27,6 +27,8 @@ Vite prints the frontend URL when it starts (normally <http://localhost:5173/>;
|
|||||||
|
|
||||||
To test the frontend against a different backend, set `VITE_DEV_API_PROXY` when starting Vite, for example `VITE_DEV_API_PROXY=https://bbs.neuf.no mise run frontend-run`. This affects the development proxy only; use caution with a live backend, since app changes and submissions may modify production data.
|
To test the frontend against a different backend, set `VITE_DEV_API_PROXY` when starting Vite, for example `VITE_DEV_API_PROXY=https://bbs.neuf.no mise run frontend-run`. This affects the development proxy only; use caution with a live backend, since app changes and submissions may modify production data.
|
||||||
|
|
||||||
|
For more realistic statistics, `uv run manage.py seed_demo_data --size medium` also generates random users, orders and deposits spread over time (`small`, `medium` or `large`; add `--seed N` for reproducible output). It adds more data on every run.
|
||||||
|
|
||||||
To create the admin during data seeding instead, set `BBS_SEED_ADMIN_PASSWORD` for that command; the command only creates a local `admin` superuser if one does not already exist.
|
To create the admin during data seeding instead, set `BBS_SEED_ADMIN_PASSWORD` for that command; the command only creates a local `admin` superuser if one does not already exist.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|||||||
@@ -1,31 +1,128 @@
|
|||||||
import os
|
import os
|
||||||
|
import random
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand, CommandParser
|
||||||
|
from django.db import transaction as db_transaction
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bbs.models import InventoryTransaction, Order, OrderLine, Product, Transaction, UserProfile
|
from bbs.models import InventoryTransaction, Order, OrderLine, Product, Transaction, UserProfile
|
||||||
|
|
||||||
|
# Some users go by first name only, which the frontend handles differently.
|
||||||
DEMO_USERS = (
|
DEMO_USERS = (
|
||||||
("demo-ada", "Ada", "Nord", "ada@example.test", Decimal("300.0")),
|
("demo-ada", "Ada", "Nord", "ada@example.test", Decimal("300.0")),
|
||||||
("demo-bjørn", "Bjørn", "Berg", "bjorn@example.test", Decimal("250.0")),
|
("demo-bjørn", "Bjørn", "Berg", "bjorn@example.test", Decimal("250.0")),
|
||||||
("demo-casey", "Casey", "Sol", "casey@example.test", Decimal("200.0")),
|
("demo-casey", "Casey", "Sol", "casey@example.test", Decimal("200.0")),
|
||||||
|
("demo-kari", "Kari", "Nordmann", "kari@example.test", Decimal("400.0")),
|
||||||
|
("demo-ole-martin", "Ole-Martin", "Ødegård", "ole-martin@example.test", Decimal("300.0")),
|
||||||
|
("demo-solveig", "Solveig", "", "solveig@example.test", Decimal("250.0")),
|
||||||
|
("demo-trygve", "Trygve", "Halvorsen", "trygve@example.test", Decimal("200.0")),
|
||||||
|
("demo-ingrid", "Ingrid", "Bakken", "ingrid@example.test", Decimal("350.0")),
|
||||||
|
("demo-håkon", "Håkon", "", "hakon@example.test", Decimal("150.0")),
|
||||||
)
|
)
|
||||||
|
|
||||||
DEMO_PRODUCTS = (
|
# name, volume_liter, alcohol_percent, sale_price_int, sale_price_ext
|
||||||
("Demo Cola", Decimal("25.0"), Decimal("30.0"), 30),
|
BEERS = (
|
||||||
("Demo Kaffe", Decimal("15.0"), Decimal("20.0"), 30),
|
("Frydenlund Fatøl", "0.5", "4.5", "25", "35"),
|
||||||
("Demo Sjokolade", Decimal("20.0"), Decimal("25.0"), 30),
|
("Ringnes Pilsner", "0.5", "4.7", "25", "35"),
|
||||||
("Demo Energidrikk", Decimal("35.0"), Decimal("40.0"), 30),
|
("Nøgne Ø India Pale Ale", "0.5", "7.5", "45", "60"),
|
||||||
|
("Lervig Lucky Jack", "0.33", "4.7", "40", "55"),
|
||||||
|
("Munkholm Alkoholfri", "0.5", "0.5", "20", "28"),
|
||||||
|
("Haandbryggeriet Mjød", "0.5", "8.0", "55", "70"),
|
||||||
|
("Hansa Bayer", "0.5", "4.6", "27", "37"),
|
||||||
|
("Aass Bayer", "0.5", "4.5", "28", "38"),
|
||||||
|
("Mack Pilsner", "0.5", "4.7", "28", "38"),
|
||||||
|
("Dahls Pils", "0.5", "4.5", "25", "35"),
|
||||||
|
("Borg Bryggeri Pils", "0.5", "4.7", "30", "40"),
|
||||||
|
("Grans Bare Pils", "0.5", "4.7", "24", "34"),
|
||||||
|
("CB Pilsner", "0.5", "4.7", "25", "35"),
|
||||||
|
("7 Fjell Hopyards IPA", "0.33", "6.5", "48", "63"),
|
||||||
|
("Tuborg Grønn", "0.5", "4.6", "28", "38"),
|
||||||
|
("Carlsberg Pilsner", "0.5", "4.6", "28", "38"),
|
||||||
|
("Frydenlund Bayer", "0.5", "4.5", "26", "36"),
|
||||||
|
("Berentsens Julebrygg", "0.5", "4.7", "32", "42"),
|
||||||
|
("Ægir Sumble", "0.5", "4.7", "42", "57"),
|
||||||
|
("Kinn Vestkyst", "0.5", "5.2", "46", "61"),
|
||||||
|
("Trondhjem Pilsner", "0.5", "4.7", "30", "40"),
|
||||||
|
("Nøgne Ø Pale Ale", "0.5", "4.5", "44", "59"),
|
||||||
|
("Voss Bryggeri Pils", "0.5", "4.7", "38", "50"),
|
||||||
|
("Lervig Pils", "0.33", "4.7", "30", "40"),
|
||||||
)
|
)
|
||||||
|
DEMO_PRODUCTS = BEERS[:6]
|
||||||
|
DEMO_STOCK = 30
|
||||||
|
|
||||||
|
DESCRIPTIONS = (
|
||||||
|
"Klassisk norsk pils med frisk og rund smak.",
|
||||||
|
"Fyldig og maltrik, perfekt på en kald kveld.",
|
||||||
|
"Lett og forfriskende, godt selskap til maten.",
|
||||||
|
"Humlerik og bitter, for den som vil ha litt mer.",
|
||||||
|
"Tradisjonsrik bryggekunst fra ærverdig bryggeri.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# user index, days ago, (product name, amount) lines
|
||||||
|
DEMO_ORDERS = (
|
||||||
|
(0, 26, (("Ringnes Pilsner", 2), ("Munkholm Alkoholfri", 1))),
|
||||||
|
(1, 21, (("Frydenlund Fatøl", 2), ("Lervig Lucky Jack", 1))),
|
||||||
|
(2, 13, (("Nøgne Ø India Pale Ale", 1),)),
|
||||||
|
(3, 24, (("Frydenlund Fatøl", 2), ("Munkholm Alkoholfri", 1))),
|
||||||
|
(4, 19, (("Nøgne Ø India Pale Ale", 2),)),
|
||||||
|
(5, 15, (("Ringnes Pilsner", 1), ("Lervig Lucky Jack", 1))),
|
||||||
|
(6, 11, (("Haandbryggeriet Mjød", 1),)),
|
||||||
|
(7, 8, (("Lervig Lucky Jack", 2), ("Frydenlund Fatøl", 1))),
|
||||||
|
(3, 4, (("Ringnes Pilsner", 1), ("Frydenlund Fatøl", 1))),
|
||||||
|
(8, 2, (("Munkholm Alkoholfri", 3),)),
|
||||||
|
(4, 1, (("Frydenlund Fatøl", 2),)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extra random users for --size
|
||||||
|
FIRST_NAMES = (
|
||||||
|
"Ola", "Kari", "Per", "Anne", "Lars", "Ingrid", "Bjørn", "Marit", "Geir", "Hanne", "Knut", "Liv",
|
||||||
|
"Nils", "Solveig", "Erik", "Astrid", "Håkon", "Sigrid", "Trygve", "Randi", "Espen", "Tone", "Magnus",
|
||||||
|
"Silje", "Henrik", "Maren", "Sander", "Ida", "Jonas", "Emma", "Mathias", "Nora", "Tobias", "Thea",
|
||||||
|
"Vegard", "Kristin", "Ole-Martin", "Anne-Lise", "Frode", "Gunnhild",
|
||||||
|
) # fmt: skip
|
||||||
|
LAST_NAMES = (
|
||||||
|
"Hansen", "Johansen", "Olsen", "Larsen", "Andersen", "Pedersen", "Nilsen", "Kristiansen", "Jensen",
|
||||||
|
"Karlsen", "Berg", "Haugen", "Hagen", "Johnsen", "Andreassen", "Jakobsen", "Dahl", "Lund", "Halvorsen",
|
||||||
|
"Eriksen", "Bakken", "Moen", "Strand", "Ødegård",
|
||||||
|
) # fmt: skip
|
||||||
|
|
||||||
|
SIZE_PRESETS = {
|
||||||
|
"small": {"users": 6, "orders": 40, "days": 30},
|
||||||
|
"medium": {"users": 15, "orders": 300, "days": 180},
|
||||||
|
"large": {"users": 40, "orders": 1500, "days": 365},
|
||||||
|
}
|
||||||
|
FIRST_NAME_ONLY_PROB = 0.35 # users registered with just a first name
|
||||||
|
EXTERNAL_ORDER_PROB = 0.10 # orders with no customer (paid in cash)
|
||||||
|
WITHDRAWAL_PROB = 0.10 # users who have had money paid back out
|
||||||
|
|
||||||
|
|
||||||
|
def avatar_url(email: str) -> str:
|
||||||
|
return f"https://i.pravatar.cc/150?u={quote(email)}"
|
||||||
|
|
||||||
|
|
||||||
|
def ascii_name(name: str) -> str:
|
||||||
|
return name.lower().replace("ø", "o").replace("å", "a").replace("æ", "ae")
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Create repeatable demo users, products, deposits, and orders for local testing."
|
help = "Create repeatable demo users, products, deposits, and orders for local testing."
|
||||||
|
|
||||||
|
def add_arguments(self, parser: CommandParser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--size",
|
||||||
|
choices=sorted(SIZE_PRESETS),
|
||||||
|
help="Also generate random users, orders and deposits on top of the demo data. Adds more on every run.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--users", type=int, help="Number of random users (overrides --size).")
|
||||||
|
parser.add_argument("--orders", type=int, help="Number of random orders (overrides --size).")
|
||||||
|
parser.add_argument("--days", type=int, help="Spread random activity over this many past days.")
|
||||||
|
parser.add_argument("--seed", type=int, help="Seed for the random data, for reproducible output.")
|
||||||
|
|
||||||
def handle(self, *args, **options): # noqa: ARG002
|
def handle(self, *args, **options): # noqa: ARG002
|
||||||
users = self.seed_users()
|
users = self.seed_users()
|
||||||
products = self.seed_products(users[0][0])
|
products = self.seed_products(users[0][0])
|
||||||
@@ -34,6 +131,22 @@ class Command(BaseCommand):
|
|||||||
self.update_balances(users)
|
self.update_balances(users)
|
||||||
self.create_admin()
|
self.create_admin()
|
||||||
|
|
||||||
|
if options["size"]:
|
||||||
|
preset = SIZE_PRESETS[options["size"]]
|
||||||
|
generator = RandomData(
|
||||||
|
random.Random(options["seed"]), # noqa: S311
|
||||||
|
days=options["days"] or preset["days"],
|
||||||
|
)
|
||||||
|
with db_transaction.atomic():
|
||||||
|
generator.run(
|
||||||
|
n_users=options["users"] or preset["users"],
|
||||||
|
n_orders=options["orders"] or preset["orders"],
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
f"Generated {generator.n_users} random users and {generator.n_orders} random orders "
|
||||||
|
f"over {generator.days} days.",
|
||||||
|
)
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS("Demo data is ready."))
|
self.stdout.write(self.style.SUCCESS("Demo data is ready."))
|
||||||
if not os.environ.get("BBS_SEED_ADMIN_PASSWORD"):
|
if not os.environ.get("BBS_SEED_ADMIN_PASSWORD"):
|
||||||
self.stdout.write("Create an admin login with: uv run manage.py createsuperuser")
|
self.stdout.write("Create an admin login with: uv run manage.py createsuperuser")
|
||||||
@@ -47,21 +160,23 @@ class Command(BaseCommand):
|
|||||||
username=username,
|
username=username,
|
||||||
defaults={"first_name": first_name, "last_name": last_name, "email": email},
|
defaults={"first_name": first_name, "last_name": last_name, "email": email},
|
||||||
)
|
)
|
||||||
UserProfile.objects.get_or_create(user=user)
|
UserProfile.objects.get_or_create(user=user, defaults={"image": avatar_url(email)})
|
||||||
users.append((user, deposit_amount))
|
users.append((user, deposit_amount))
|
||||||
return users
|
return users
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def seed_products(stock_user):
|
def seed_products(stock_user):
|
||||||
products = {}
|
products = {}
|
||||||
for name, sale_price_int, sale_price_ext, stock in DEMO_PRODUCTS:
|
for index, (name, volume, alcohol, sale_price_int, sale_price_ext) in enumerate(DEMO_PRODUCTS):
|
||||||
product, created = Product.objects.get_or_create(
|
product, created = Product.objects.get_or_create(
|
||||||
name=name,
|
name=name,
|
||||||
defaults={
|
defaults={
|
||||||
"sale_price_int": sale_price_int,
|
"sale_price_int": Decimal(sale_price_int),
|
||||||
"sale_price_ext": sale_price_ext,
|
"sale_price_ext": Decimal(sale_price_ext),
|
||||||
"inventory_amount": stock,
|
"volume_liter": Decimal(volume),
|
||||||
"description": "Demo product for local development.",
|
"alcohol_percent": Decimal(alcohol),
|
||||||
|
"inventory_amount": DEMO_STOCK,
|
||||||
|
"description": DESCRIPTIONS[index % len(DESCRIPTIONS)],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
products[name] = product
|
products[name] = product
|
||||||
@@ -70,7 +185,7 @@ class Command(BaseCommand):
|
|||||||
user=stock_user,
|
user=stock_user,
|
||||||
product=product,
|
product=product,
|
||||||
comment="Demo opening stock",
|
comment="Demo opening stock",
|
||||||
defaults={"amount": stock, "unit_price": Decimal("10.0")},
|
defaults={"amount": DEMO_STOCK, "unit_price": Decimal(sale_price_int) * Decimal("0.6")},
|
||||||
)
|
)
|
||||||
return products
|
return products
|
||||||
|
|
||||||
@@ -87,14 +202,7 @@ class Command(BaseCommand):
|
|||||||
deposit.save(update_fields=["created"])
|
deposit.save(update_fields=["created"])
|
||||||
|
|
||||||
def seed_orders(self, users, products):
|
def seed_orders(self, users, products):
|
||||||
orders = (
|
for user_index, days_ago, lines in DEMO_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]
|
user = users[user_index][0]
|
||||||
created_at = self.timestamp(days_ago=days_ago)
|
created_at = self.timestamp(days_ago=days_ago)
|
||||||
total = sum(
|
total = sum(
|
||||||
@@ -149,3 +257,132 @@ class Command(BaseCommand):
|
|||||||
def timestamp(days_ago: int) -> datetime:
|
def timestamp(days_ago: int) -> datetime:
|
||||||
date = timezone.localdate() - timedelta(days=days_ago)
|
date = timezone.localdate() - timedelta(days=days_ago)
|
||||||
return timezone.make_aware(datetime.combine(date, time(hour=12)))
|
return timezone.make_aware(datetime.combine(date, time(hour=12)))
|
||||||
|
|
||||||
|
|
||||||
|
class RandomData:
|
||||||
|
"""Random users, restocks, orders and deposits spread over time, for realistic stats."""
|
||||||
|
|
||||||
|
def __init__(self, rng: random.Random, days: int):
|
||||||
|
self.rng = rng
|
||||||
|
self.days = days
|
||||||
|
self.end = timezone.now()
|
||||||
|
self.start = self.end - timedelta(days=days)
|
||||||
|
self.n_users = 0
|
||||||
|
self.n_orders = 0
|
||||||
|
|
||||||
|
def run(self, n_users: int, n_orders: int):
|
||||||
|
users = self.create_users(n_users)
|
||||||
|
products = self.create_products()
|
||||||
|
self.restock(products, users)
|
||||||
|
spent = self.create_orders(users, products, n_orders)
|
||||||
|
self.create_money(users, spent)
|
||||||
|
|
||||||
|
def random_time(self):
|
||||||
|
span = int((self.end - self.start).total_seconds())
|
||||||
|
return self.start + timedelta(seconds=self.rng.randint(0, max(span, 1)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_created(obj, created):
|
||||||
|
# created is auto_now_add, so backdate with an update
|
||||||
|
type(obj).objects.filter(pk=obj.pk).update(created=created)
|
||||||
|
|
||||||
|
def create_users(self, n: int) -> list:
|
||||||
|
user_model = get_user_model()
|
||||||
|
users = []
|
||||||
|
for _ in range(n):
|
||||||
|
first = self.rng.choice(FIRST_NAMES)
|
||||||
|
last = "" if self.rng.random() < FIRST_NAME_ONLY_PROB else self.rng.choice(LAST_NAMES)
|
||||||
|
base = ".".join(ascii_name(part) for part in (first, last) if part)
|
||||||
|
email = f"{base}@example.test"
|
||||||
|
suffix = 1
|
||||||
|
while user_model.objects.filter(username=email).exists():
|
||||||
|
suffix += 1
|
||||||
|
email = f"{base}{suffix}@example.test"
|
||||||
|
|
||||||
|
user = user_model.objects.create_user(username=email, email=email, first_name=first, last_name=last)
|
||||||
|
UserProfile.objects.create(user=user, image=avatar_url(email))
|
||||||
|
users.append(user)
|
||||||
|
self.n_users = len(users)
|
||||||
|
return users
|
||||||
|
|
||||||
|
def create_products(self) -> list:
|
||||||
|
products = []
|
||||||
|
for name, volume, alcohol, sale_price_int, sale_price_ext in BEERS:
|
||||||
|
product, _ = Product.objects.get_or_create(
|
||||||
|
name=name,
|
||||||
|
defaults={
|
||||||
|
"sale_price_int": Decimal(sale_price_int),
|
||||||
|
"sale_price_ext": Decimal(sale_price_ext),
|
||||||
|
"volume_liter": Decimal(volume),
|
||||||
|
"alcohol_percent": Decimal(alcohol),
|
||||||
|
"description": self.rng.choice(DESCRIPTIONS),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
products.append(product)
|
||||||
|
return products
|
||||||
|
|
||||||
|
def restock(self, products: list, users: list):
|
||||||
|
for product in products:
|
||||||
|
wholesale = (product.sale_price_int * Decimal("0.6")).quantize(Decimal("0.1"))
|
||||||
|
for _ in range(self.rng.randint(1, 3)):
|
||||||
|
amount = self.rng.choice([24, 48, 60, 120])
|
||||||
|
restock = InventoryTransaction.objects.create(
|
||||||
|
user=self.rng.choice(users),
|
||||||
|
product=product,
|
||||||
|
amount=amount,
|
||||||
|
unit_price=wholesale,
|
||||||
|
comment="Innkjøp",
|
||||||
|
)
|
||||||
|
self.set_created(restock, self.random_time())
|
||||||
|
product.inventory_amount += amount
|
||||||
|
product.save(update_fields=["inventory_amount"])
|
||||||
|
|
||||||
|
def create_orders(self, users: list, products: list, n_orders: int) -> dict:
|
||||||
|
spent = {user.pk: Decimal(0) for user in users}
|
||||||
|
for _ in range(n_orders):
|
||||||
|
customer = None if self.rng.random() < EXTERNAL_ORDER_PROB else self.rng.choice(users)
|
||||||
|
# Mostly a single product, occasionally two or three
|
||||||
|
n_lines = self.rng.choices([1, 2, 3], weights=[60, 30, 10])[0]
|
||||||
|
lines = []
|
||||||
|
order_sum = Decimal(0)
|
||||||
|
for product in self.rng.sample(products, n_lines):
|
||||||
|
amount = self.rng.choices([1, 2, 3, 6], weights=[55, 30, 10, 5])[0]
|
||||||
|
unit_price = product.sale_price_int if customer else product.sale_price_ext
|
||||||
|
lines.append(OrderLine(product=product, amount=amount, unit_price=unit_price))
|
||||||
|
order_sum += unit_price * amount
|
||||||
|
product.inventory_amount = max(0, product.inventory_amount - amount)
|
||||||
|
|
||||||
|
order = Order.objects.create(customer=customer, order_sum=order_sum)
|
||||||
|
for line in lines:
|
||||||
|
line.order = order
|
||||||
|
OrderLine.objects.bulk_create(lines)
|
||||||
|
self.set_created(order, self.random_time())
|
||||||
|
if customer:
|
||||||
|
spent[customer.pk] += order_sum
|
||||||
|
|
||||||
|
for product in products:
|
||||||
|
product.save(update_fields=["inventory_amount"])
|
||||||
|
self.n_orders = n_orders
|
||||||
|
return spent
|
||||||
|
|
||||||
|
def create_money(self, users: list, spent: dict):
|
||||||
|
"""Round deposits covering what each user spent, plus rare withdrawals within the leftover buffer."""
|
||||||
|
for user in users:
|
||||||
|
buffer = Decimal(self.rng.choice([0, 50, 100, 200, 300, 400]))
|
||||||
|
remaining = spent[user.pk] + buffer
|
||||||
|
net = Decimal(0)
|
||||||
|
while remaining > 0:
|
||||||
|
amount = min(Decimal(self.rng.choice([100, 200, 200, 300, 500])), remaining)
|
||||||
|
deposit = Transaction.objects.create(user=user, amount=amount)
|
||||||
|
self.set_created(deposit, self.random_time())
|
||||||
|
net += amount
|
||||||
|
remaining -= amount
|
||||||
|
|
||||||
|
if buffer > 0 and self.rng.random() < WITHDRAWAL_PROB:
|
||||||
|
withdrawal = Decimal(self.rng.randint(1, int(buffer)))
|
||||||
|
payout = Transaction.objects.create(user=user, amount=-withdrawal)
|
||||||
|
self.set_created(payout, self.random_time())
|
||||||
|
net -= withdrawal
|
||||||
|
|
||||||
|
user.profile.balance = net - spent[user.pk]
|
||||||
|
user.profile.save(update_fields=["balance"])
|
||||||
@@ -4,7 +4,10 @@ from django.core.management import call_command
|
|||||||
from django.test import Client
|
from django.test import Client
|
||||||
from strawberry.django.test import GraphQLTestClient
|
from strawberry.django.test import GraphQLTestClient
|
||||||
|
|
||||||
from bbs.models import InventoryTransaction, Order, Product, Transaction
|
from bbs.management.commands.seed_demo_data import DEMO_PRODUCTS
|
||||||
|
from bbs.models import InventoryTransaction, Order, Product, Transaction, UserProfile
|
||||||
|
|
||||||
|
DEMO_PRODUCT_NAMES = [name for name, *_ in DEMO_PRODUCTS]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -12,12 +15,14 @@ def test_seed_demo_data_is_repeatable():
|
|||||||
call_command("seed_demo_data")
|
call_command("seed_demo_data")
|
||||||
call_command("seed_demo_data")
|
call_command("seed_demo_data")
|
||||||
|
|
||||||
assert get_user_model().objects.filter(username__startswith="demo-").count() == 3
|
assert get_user_model().objects.filter(username__startswith="demo-").count() == 9
|
||||||
assert Product.objects.filter(name__startswith="Demo ").count() == 4
|
assert Product.objects.filter(name__in=DEMO_PRODUCT_NAMES).count() == 6
|
||||||
assert Order.objects.count() == 5
|
assert Order.objects.count() == 11
|
||||||
assert Transaction.objects.count() == 3
|
assert Transaction.objects.count() == 9
|
||||||
assert InventoryTransaction.objects.count() == 4
|
assert InventoryTransaction.objects.count() == 6
|
||||||
assert sum(product.inventory_amount for product in Product.objects.filter(name__startswith="Demo ")) == 109
|
assert sum(product.inventory_amount for product in Product.objects.filter(name__in=DEMO_PRODUCT_NAMES)) == 155
|
||||||
|
assert all(profile.balance >= 0 for profile in UserProfile.objects.all())
|
||||||
|
assert all("pravatar.cc" in profile.image for profile in UserProfile.objects.all())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -38,8 +43,8 @@ def test_demo_data_is_available_when_an_admin_has_no_profile():
|
|||||||
|
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data
|
assert result.data
|
||||||
assert len(result.data["allProducts"]) == 4
|
assert len(result.data["allProducts"]) == 6
|
||||||
assert len(result.data["allUsers"]) == 3
|
assert len(result.data["allUsers"]) == 9
|
||||||
|
|
||||||
ada = user_model.objects.get(username="demo-ada")
|
ada = user_model.objects.get(username="demo-ada")
|
||||||
detail_result = gql_client.query(
|
detail_result = gql_client.query(
|
||||||
@@ -63,3 +68,26 @@ def test_demo_data_is_available_when_an_admin_has_no_profile():
|
|||||||
assert not detail_result.errors
|
assert not detail_result.errors
|
||||||
assert detail_result.data["user"]["profile"]["topMonths"]
|
assert detail_result.data["user"]["profile"]["topMonths"]
|
||||||
assert isinstance(detail_result.data["user"]["profile"]["topMonths"][0]["sum"], float)
|
assert isinstance(detail_result.data["user"]["profile"]["topMonths"][0]["sum"], float)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_seed_demo_data_random_size_adds_consistent_data():
|
||||||
|
call_command("seed_demo_data", size="small", seed=1, users=5, orders=30)
|
||||||
|
|
||||||
|
random_users = get_user_model().objects.exclude(username__startswith="demo-")
|
||||||
|
assert random_users.count() == 5
|
||||||
|
assert Order.objects.count() == 11 + 30
|
||||||
|
for user in random_users:
|
||||||
|
deposits = sum(t.amount for t in Transaction.objects.filter(user=user))
|
||||||
|
purchases = sum(o.order_sum for o in Order.objects.filter(customer=user))
|
||||||
|
assert user.profile.balance == deposits - purchases
|
||||||
|
assert user.profile.balance >= 0
|
||||||
|
assert all(product.inventory_amount >= 0 for product in Product.objects.all())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_seed_demo_data_random_size_can_run_twice():
|
||||||
|
call_command("seed_demo_data", size="small", seed=1, users=5, orders=5)
|
||||||
|
call_command("seed_demo_data", size="small", seed=1, users=5, orders=5)
|
||||||
|
|
||||||
|
assert get_user_model().objects.exclude(username__startswith="demo-").count() == 10
|
||||||
Reference in new issue
Block a user