Add shopper accounts, reviews, tracking, and seed_demo (#9)

Closes #9. Shop-gated buyer accounts, purchase reviews, Stripe customer ids, shipment tracking, slim public contact form, and a template-neutral seed_demo command.
This commit is contained in:
2026-09-07 08:35:55 -05:00
parent 9cdce7a897
commit 5b11cc18c7
66 changed files with 3051 additions and 285 deletions
+848
View File
@@ -0,0 +1,848 @@
"""Populate a demo catalog, portal lists, and shop traffic for client walkthroughs.
Safe on DJANGO_ENV=dev and beta only. Never runs against prod. Does not send
mail or call EasyPost/Stripe. Re-runs are idempotent (DEMO- / demo+ keys).
Pass --reset to wipe tagged demo rows and seed again.
uv run python manage.py seed_demo
docker compose exec web uv run python manage.py seed_demo --reset
"""
from __future__ import annotations
import os
from datetime import timedelta
from decimal import Decimal
from django.apps import apps
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
DEMO_NOTE = "[demo-seed]"
DEMO_UTM_CAMPAIGN = "site-demo"
DEMO_EMAIL_DOMAIN = "@example.com"
DEMO_EMAIL_PREFIX = "demo+"
DEMO_SHOPPER_PASSWORD = "Demo-buyer-pass-123"
DEMO_SHOPPER_SLUGS = ("jordan", "casey", "sam")
def _django_env() -> str:
return (os.environ.get("DJANGO_ENV") or "dev").lower()
def _stamp(model, pk, when) -> None:
model.objects.filter(pk=pk).update(created_at=when, updated_at=when)
class Command(BaseCommand):
help = (
"Seed tagged fake catalog, shoppers, reviews, tracking, contacts, "
"leads, orders, campaigns, and analytics for client demos. "
"Allowed on DJANGO_ENV=dev and beta only. Demo shoppers use "
f"{DEMO_EMAIL_PREFIX}jordan{DEMO_EMAIL_DOMAIN} / {DEMO_SHOPPER_PASSWORD}."
)
def add_arguments(self, parser):
parser.add_argument(
"--reset",
action="store_true",
help="Delete previously tagged demo rows, then seed again.",
)
def handle(self, *args, **options):
env = _django_env()
if env == "prod":
raise CommandError(
"seed_demo refuses DJANGO_ENV=prod. Run it on dev or beta."
)
self.verbosity = int(options.get("verbosity", 1))
self.now = timezone.now()
if options["reset"]:
self._reset()
self.stdout.write("Cleared tagged demo rows.")
User = get_user_model()
self.owner = User.objects.filter(is_staff=True).order_by("pk").first()
if self.owner is None:
self.owner = (
User.objects.exclude(username__startswith=DEMO_EMAIL_PREFIX)
.order_by("pk")
.first()
)
counts = {
"contacts": self._seed_contacts(),
"leads": self._seed_leads(),
"analytics": self._seed_analytics(),
}
if apps.is_installed("shop"):
counts["products"] = self._seed_products()
counts["shoppers"] = self._seed_shoppers()
counts["orders"] = self._seed_orders()
counts["reviews"] = self._seed_reviews()
if apps.is_installed("shipping"):
counts["shipments"] = self._seed_shipments()
if apps.is_installed("payments"):
counts["invoices"] = self._seed_invoices()
if apps.is_installed("email_sms"):
counts["campaigns"] = self._seed_campaigns()
summary = ", ".join(f"{key}={value}" for key, value in counts.items())
self.stdout.write(self.style.SUCCESS(f"Demo seed ready on {env} ({summary})."))
if apps.is_installed("shop"):
self.stdout.write(
"Demo shopper: "
f"{DEMO_EMAIL_PREFIX}jordan{DEMO_EMAIL_DOMAIN} / {DEMO_SHOPPER_PASSWORD}"
)
def _log(self, message: str) -> None:
if self.verbosity >= 2:
self.stdout.write(message)
def _reset(self) -> None:
from analytics.models import UTMVisit
from contacts.models import Contact
from leads.models import Lead
if apps.is_installed("shipping"):
from shipping.models import Shipment
Shipment.objects.filter(order__number__startswith="DEMO-").delete()
if apps.is_installed("shop"):
from shop.models import Order, Product
Order.objects.filter(number__startswith="DEMO-").delete()
Product.objects.filter(sku__startswith="DEMO-").delete()
if apps.is_installed("payments"):
from payments.models import Invoice
Invoice.objects.filter(number__startswith="DEMO-").delete()
if apps.is_installed("email_sms"):
from email_sms.models import Campaign
Campaign.objects.filter(name__startswith="DEMO:").delete()
Lead.objects.filter(message__startswith=DEMO_NOTE).delete()
Contact.objects.filter(
email__startswith=DEMO_EMAIL_PREFIX,
email__endswith=DEMO_EMAIL_DOMAIN,
).delete()
User = get_user_model()
User.objects.filter(
username__startswith=DEMO_EMAIL_PREFIX,
username__endswith=DEMO_EMAIL_DOMAIN,
is_staff=False,
).delete()
UTMVisit.objects.filter(utm_campaign=DEMO_UTM_CAMPAIGN).delete()
def _seed_contacts(self) -> int:
from contacts.consent import set_channel_consent
from contacts.models import Channel, Contact
people = (
(
"maya",
"Maya",
"Chen",
"630-555-0142",
Contact.Source.CONTACT_FORM,
True,
True,
"Naperville",
"Asked about bulk pricing.",
),
(
"jordan",
"Jordan",
"Walsh",
"630-555-0198",
Contact.Source.MANUAL,
True,
False,
"Aurora",
"Repeat buyer.",
),
(
"priya",
"Priya",
"Shah",
"847-555-0110",
Contact.Source.IMPORT,
True,
True,
"Wheaton",
"Event favor quote.",
),
(
"evan",
"Evan",
"Brooks",
"312-555-0177",
Contact.Source.NOTIFY_ME,
False,
True,
"Chicago",
"Notify list from the coming-soon page.",
),
(
"sam",
"Sam",
"Ortiz",
"630-555-0166",
Contact.Source.CONTACT_FORM,
True,
True,
"Lisle",
"Team gifts.",
),
(
"riley",
"Riley",
"Nguyen",
"708-555-0133",
Contact.Source.OTHER,
True,
False,
"Downers Grove",
"Asked about made-to-order items.",
),
(
"alex",
"Alex",
"Patel",
"630-555-0121",
Contact.Source.MANUAL,
False,
False,
"Naperville",
"Opted out of email after one campaign.",
),
(
"casey",
"Casey",
"Miller",
"815-555-0188",
Contact.Source.IMPORT,
True,
True,
"Geneva",
"School fair bulk order.",
),
)
created = 0
for slug, first, last, phone, source, email_on, sms_on, city, note in people:
email = f"{DEMO_EMAIL_PREFIX}{slug}{DEMO_EMAIL_DOMAIN}"
contact, was_created = Contact.objects.update_or_create(
email=email,
defaults={
"first_name": first,
"last_name": last,
"phone": phone,
"source": source,
"notes": f"{DEMO_NOTE} {note}",
"postal_address": Contact.make_postal_address(
line1=f"{100 + len(slug) * 17} Demo Ave",
city=city,
state="IL",
zip_code="60540",
),
},
)
set_channel_consent(
contact, Channel.EMAIL, opted_in=email_on, reason="demo_seed"
)
set_channel_consent(
contact, Channel.SMS, opted_in=sms_on, reason="demo_seed"
)
if was_created:
created += 1
self._log(f"contact {contact.email}")
return created
def _seed_leads(self) -> int:
from analytics.models import Attribution
from contacts.models import Contact
from leads.models import Lead, LeadNote
specs = (
(
"maya",
Lead.Status.NEW,
"Need a dozen units by next Friday.",
"instagram",
"social",
2,
),
(
"priya",
Lead.Status.CONTACTED,
"Quoted 40 keychains. Waiting on names.",
"google",
"cpc",
8,
),
(
"casey",
Lead.Status.WON,
"Fair kits — 30 tote bags, paid via invoice.",
"google",
"organic",
18,
),
(
"riley",
Lead.Status.LOST,
"Wanted 200 units at a price we could not hit.",
"",
"",
12,
),
)
created = 0
for slug, status, body, source, medium, days_ago in specs:
email = f"{DEMO_EMAIL_PREFIX}{slug}{DEMO_EMAIL_DOMAIN}"
contact = Contact.objects.filter(email=email).first()
if contact is None:
continue
message = f"{DEMO_NOTE} {body}"
lead, was_created = Lead.objects.get_or_create(
contact=contact,
message=message,
defaults={"status": status, "owner": self.owner},
)
if not was_created:
if lead.status != status:
lead.status = status
lead.owner = self.owner
lead.save(update_fields=["status", "owner", "updated_at"])
else:
created += 1
LeadNote.objects.create(
lead=lead,
author=self.owner,
body=f"{DEMO_NOTE} Demo walkthrough note.",
)
when = self.now - timedelta(days=days_ago)
_stamp(Lead, lead.pk, when)
if source:
Attribution.objects.update_or_create(
lead=lead,
defaults={
"utm_source": source,
"utm_medium": medium,
"utm_campaign": DEMO_UTM_CAMPAIGN,
},
)
self._log(f"lead {slug} {status}")
return created
def _seed_analytics(self) -> int:
from analytics.models import PageView, UTMVisit
if UTMVisit.objects.filter(utm_campaign=DEMO_UTM_CAMPAIGN).exists():
return 0
paths = (
"/",
"/",
"/",
"/shop/",
"/shop/",
"/shop/demo-notebook/",
"/shop/demo-tote/",
"/about/",
"/contact/",
)
created = 0
for offset, path in enumerate(paths * 6):
when = self.now - timedelta(hours=4 * offset + 3)
view = PageView.objects.create(path=path)
_stamp(PageView, view.pk, when)
created += 1
sources = (
("google", "cpc", "/shop/"),
("google", "organic", "/"),
("instagram", "social", "/shop/demo-notebook/"),
("facebook", "social", "/contact/"),
("direct", "", "/"),
)
for i, (source, medium, path) in enumerate(sources * 8):
when = self.now - timedelta(hours=6 * i + 2)
visit = UTMVisit.objects.create(
correlation_id=f"demo-{i:03d}",
path=path,
utm_source=source,
utm_medium=medium,
utm_campaign=DEMO_UTM_CAMPAIGN,
user_agent="Demo seed",
)
_stamp(UTMVisit, visit.pk, when)
created += 1
return created
def _seed_products(self) -> int:
from shop.models import Product
catalog = (
{
"sku": "DEMO-NOTEBOOK",
"name": "Notebook",
"price": Decimal("18.00"),
"fulfillment": Product.Fulfillment.STOCKED,
"stock_qty": 24,
"print_minutes": 0,
"filament_grams": 0,
"published": True,
"description": "Lined notebook. A simple catalog item for demos.",
},
{
"sku": "DEMO-STICKER",
"name": "Sticker pack",
"price": Decimal("8.00"),
"fulfillment": Product.Fulfillment.STOCKED,
"stock_qty": 40,
"print_minutes": 0,
"filament_grams": 0,
"published": True,
"description": "Pack of five stickers.",
},
{
"sku": "DEMO-TOTE",
"name": "Tote bag",
"price": Decimal("22.00"),
"fulfillment": Product.Fulfillment.STOCKED,
"stock_qty": 12,
"print_minutes": 0,
"filament_grams": 0,
"published": True,
"description": "Canvas tote. Good for a fulfilled demo order.",
},
{
"sku": "DEMO-MUG",
"name": "Ceramic mug",
"price": Decimal("16.00"),
"fulfillment": Product.Fulfillment.MADE_TO_ORDER,
"stock_qty": 0,
"print_minutes": 45,
"filament_grams": 0,
"published": True,
"description": "Printed to order. Used as the guest checkout demo.",
},
{
"sku": "DEMO-KEYCHAIN",
"name": "Custom keychain",
"price": Decimal("12.00"),
"fulfillment": Product.Fulfillment.MADE_TO_ORDER,
"stock_qty": 0,
"print_minutes": 20,
"filament_grams": 0,
"published": True,
"description": "Made to order with a name on the loop.",
},
{
"sku": "DEMO-PROTO",
"name": "Draft product (coming soon)",
"price": Decimal("45.00"),
"fulfillment": Product.Fulfillment.MADE_TO_ORDER,
"stock_qty": 0,
"print_minutes": 90,
"filament_grams": 0,
"published": False,
"description": "Unpublished so the portal list is not empty of drafts.",
},
)
created = 0
for spec in catalog:
product, was_created = Product.objects.update_or_create(
sku=spec["sku"],
defaults={
"name": spec["name"],
"slug": spec["sku"].lower(),
"description": spec["description"],
"price": spec["price"],
"currency": "usd",
"fulfillment": spec["fulfillment"],
"stock_qty": spec["stock_qty"],
"print_minutes": spec["print_minutes"],
"filament_grams": spec["filament_grams"],
"is_published": spec["published"],
"track_inventory": True,
},
)
if was_created:
created += 1
self._log(f"product {product.sku}")
return created
def _seed_shoppers(self) -> int:
from accounts.services import get_customer_profile
from contacts.models import Contact
User = get_user_model()
created = 0
for slug in DEMO_SHOPPER_SLUGS:
email = f"{DEMO_EMAIL_PREFIX}{slug}{DEMO_EMAIL_DOMAIN}"
contact = Contact.objects.filter(email=email).first()
if contact is None:
continue
user = User.objects.filter(username__iexact=email).first()
if user is None:
user = User.objects.create_user(
username=email,
email=email,
password=DEMO_SHOPPER_PASSWORD,
first_name=contact.first_name,
last_name=contact.last_name,
is_staff=False,
)
created += 1
else:
user.first_name = contact.first_name
user.last_name = contact.last_name
user.email = email
user.is_staff = False
user.set_password(DEMO_SHOPPER_PASSWORD)
user.save(
update_fields=[
"first_name",
"last_name",
"email",
"is_staff",
"password",
]
)
profile = get_customer_profile(user)
profile.phone = contact.phone
profile.shipping_address = contact.postal_address or {}
profile.save(update_fields=["phone", "shipping_address", "updated_at"])
self._log(f"shopper {email}")
return created
def _seed_orders(self) -> int:
from contacts.models import Contact
from shop.models import Order, OrderItem, Product
notebook = Product.objects.filter(sku="DEMO-NOTEBOOK").first()
sticker = Product.objects.filter(sku="DEMO-STICKER").first()
tote = Product.objects.filter(sku="DEMO-TOTE").first()
mug = Product.objects.filter(sku="DEMO-MUG").first()
if not all([notebook, sticker, tote, mug]):
return 0
specs = (
(
"DEMO-ORD-001",
"jordan",
Order.Status.PAID,
((notebook, 1),),
3,
),
(
"DEMO-ORD-002",
"casey",
Order.Status.FULFILLED,
((tote, 2), (sticker, 1)),
11,
),
(
"DEMO-ORD-003",
"sam",
Order.Status.PAID,
((sticker, 2),),
1,
),
(
"DEMO-ORD-004",
"riley",
Order.Status.OPEN,
((mug, 1),),
0,
),
(
"DEMO-ORD-005",
"alex",
Order.Status.CANCELLED,
((notebook, 1),),
16,
),
)
created = 0
User = get_user_model()
for number, slug, status, lines, days_ago in specs:
contact = Contact.objects.filter(
email=f"{DEMO_EMAIL_PREFIX}{slug}{DEMO_EMAIL_DOMAIN}"
).first()
if contact is None:
continue
buyer = User.objects.filter(username__iexact=contact.email).first()
amount = sum(
(product.price * qty for product, qty in lines),
Decimal("0.00"),
)
paid = status in {Order.Status.PAID, Order.Status.FULFILLED}
when = self.now - timedelta(days=days_ago, hours=5)
order, was_created = Order.objects.update_or_create(
number=number,
defaults={
"user": buyer,
"email": contact.email,
"customer_name": contact.full_name,
"status": status,
"amount": amount,
"currency": "usd",
"paid_at": when if paid else None,
"shipping_address": contact.postal_address,
"notes": f"{DEMO_NOTE} Demo order.",
},
)
if was_created:
created += 1
if not order.items.exists():
for product, qty in lines:
OrderItem.objects.create(
order=order,
product=product,
name=product.name,
sku=product.sku,
quantity=qty,
unit_price=product.price,
print_minutes=product.print_minutes,
)
_stamp(Order, order.pk, when)
self._log(f"order {order.number} {status}")
return created
def _seed_reviews(self) -> int:
from shop.models import Order, Product, ProductReview
User = get_user_model()
specs = (
(
"jordan",
"DEMO-ORD-001",
"DEMO-NOTEBOOK",
5,
"Solid notebook",
"Paper quality is great for daily notes.",
),
(
"casey",
"DEMO-ORD-002",
"DEMO-TOTE",
4,
"Sturdy tote",
"Held up for the school fair. Would buy again.",
),
(
"sam",
"DEMO-ORD-003",
"DEMO-STICKER",
5,
"Bright stickers",
"Colors popped. Kids loved them.",
),
)
created = 0
for slug, order_number, sku, rating, title, body in specs:
email = f"{DEMO_EMAIL_PREFIX}{slug}{DEMO_EMAIL_DOMAIN}"
user = User.objects.filter(username__iexact=email).first()
order = Order.objects.filter(number=order_number, user=user).first()
product = Product.objects.filter(sku=sku).first()
if user is None or order is None or product is None:
continue
review, was_created = ProductReview.objects.update_or_create(
user=user,
product=product,
defaults={
"order": order,
"rating": rating,
"title": title,
"body": f"{DEMO_NOTE} {body}",
},
)
if was_created:
created += 1
self._log(f"review {sku} {rating}")
return created
def _seed_shipments(self) -> int:
from shipping.models import Shipment
from shop.models import Order
specs = (
(
"DEMO-ORD-002",
{
"status": Shipment.Status.LABELED,
"carrier": "USPS",
"service": "Priority",
"tracking_number": "940011189922DEMO02",
"tracking_status": Shipment.TrackingStatus.DELIVERED,
"tracking_url": (
"https://tools.usps.com/go/TrackConfirmAction"
"?tLabels=940011189922DEMO02"
),
"rate_amount": Decimal("8.45"),
"currency": "usd",
"weight_oz": 18,
"last_tracked_at": self.now - timedelta(hours=6),
"tracking_events": [
{
"status": "in_transit",
"message": "Departed USPS facility",
"datetime": (self.now - timedelta(days=2)).isoformat(),
"location": "Chicago IL",
},
{
"status": "delivered",
"message": "Delivered, front door",
"datetime": (self.now - timedelta(days=1)).isoformat(),
"location": "Geneva IL",
},
],
"notes": f"{DEMO_NOTE} Stub delivered label. No EasyPost call.",
},
),
(
"DEMO-ORD-001",
{
"status": Shipment.Status.LABELED,
"carrier": "USPS",
"service": "GroundAdvantage",
"tracking_number": "940011189922DEMO01",
"tracking_status": Shipment.TrackingStatus.IN_TRANSIT,
"tracking_url": (
"https://tools.usps.com/go/TrackConfirmAction"
"?tLabels=940011189922DEMO01"
),
"rate_amount": Decimal("5.40"),
"currency": "usd",
"weight_oz": 12,
"last_tracked_at": self.now - timedelta(hours=2),
"tracking_events": [
{
"status": "pre_transit",
"message": "Shipping label created",
"datetime": (self.now - timedelta(days=1)).isoformat(),
"location": "Aurora IL",
},
{
"status": "in_transit",
"message": "Arrived at USPS origin facility",
"datetime": (self.now - timedelta(hours=8)).isoformat(),
"location": "Chicago IL",
},
],
"notes": f"{DEMO_NOTE} Stub in-transit label. No EasyPost call.",
},
),
)
created = 0
for number, defaults in specs:
order = Order.objects.filter(number=number).first()
if order is None:
continue
shipment = Shipment.objects.filter(order=order).first()
if shipment is None:
Shipment.objects.create(order=order, **defaults)
created += 1
else:
for key, value in defaults.items():
setattr(shipment, key, value)
shipment.save()
self._log(f"shipment {number} {defaults['tracking_status']}")
return created
def _seed_invoices(self) -> int:
from contacts.models import Contact
from payments.models import Invoice
specs = (
(
"DEMO-INV-001",
"casey",
Invoice.Status.PAID,
Decimal("210.00"),
"Fair tote kits (30)",
14,
),
(
"DEMO-INV-002",
"priya",
Invoice.Status.OPEN,
Decimal("480.00"),
"Event keychains (40) — deposit due",
4,
),
)
created = 0
for number, slug, status, amount, description, days_ago in specs:
contact = Contact.objects.filter(
email=f"{DEMO_EMAIL_PREFIX}{slug}{DEMO_EMAIL_DOMAIN}"
).first()
if contact is None:
continue
when = self.now - timedelta(days=days_ago)
paid = status == Invoice.Status.PAID
invoice, was_created = Invoice.objects.update_or_create(
number=number,
defaults={
"contact": contact,
"description": description,
"amount": amount,
"currency": "usd",
"status": status,
"paid_at": when if paid else None,
"created_by": self.owner,
"notes": f"{DEMO_NOTE} Demo invoice. No Stripe session.",
},
)
if was_created:
created += 1
_stamp(Invoice, invoice.pk, when)
self._log(f"invoice {invoice.number} {status}")
return created
def _seed_campaigns(self) -> int:
from email_sms.models import Campaign, Message
from email_sms.services import create_campaign_draft
created = 0
draft_name = "DEMO: New arrivals"
if not Campaign.objects.filter(name=draft_name).exists():
create_campaign_draft(
name=draft_name,
audience=Campaign.Audience.EMAIL_OPT_IN,
subject="New items just landed",
body="<p>Restocked notebooks and tote bags. Shop the drop.</p>",
created_by=self.owner,
)
created += 1
done_name = "DEMO: Event recap"
if not Campaign.objects.filter(name=done_name).exists():
campaign = create_campaign_draft(
name=done_name,
audience=Campaign.Audience.EMAIL_OPT_IN,
subject="Thanks for coming",
body="<p>Your order is packed. Tracking goes out tomorrow.</p>",
created_by=self.owner,
)
campaign.status = Campaign.Status.COMPLETED
campaign.notify_sent_at = self.now - timedelta(days=9)
campaign.save(update_fields=["status", "notify_sent_at", "updated_at"])
messages = list(campaign.messages.all())
for index, message in enumerate(messages):
message.status = (
Message.Status.OPENED
if index % 3 == 0
else Message.Status.DELIVERED
)
message.sent_at = self.now - timedelta(days=10)
message.save(update_fields=["status", "sent_at", "updated_at"])
created += 1
self._log(f"campaign {done_name} completed")
sms_name = "DEMO: Order update SMS"
if not Campaign.objects.filter(name=sms_name).exists():
create_campaign_draft(
name=sms_name,
audience=Campaign.Audience.SMS_OPT_IN,
body="Your order is packing. We will text when it ships.",
created_by=self.owner,
)
created += 1
return created
+111
View File
@@ -1,4 +1,6 @@
import os
from io import StringIO
from unittest.mock import patch
from django.core.management import call_command
from django.test import Client, TestCase, override_settings
@@ -30,6 +32,11 @@ class PublicSmokeTests(TestCase):
client = Client()
self.assertEqual(client.get(reverse("public:about")).status_code, 200)
self.assertEqual(client.get(reverse("public:contact")).status_code, 200)
contact = client.get(reverse("public:contact"))
self.assertContains(contact, "contact-email")
self.assertNotContains(contact, "contact-first-name")
self.assertNotContains(contact, "contact-phone")
self.assertNotContains(contact, "contact-address-line1")
class DispatchDueTests(TestCase):
@@ -37,3 +44,107 @@ class DispatchDueTests(TestCase):
out = StringIO()
call_command("dispatch_due", stdout=out)
self.assertEqual(out.getvalue(), "")
class SeedDemoTests(TestCase):
def test_refuses_prod(self):
from django.core.management.base import CommandError
with patch.dict(os.environ, {"DJANGO_ENV": "prod"}):
with self.assertRaises(CommandError) as ctx:
call_command("seed_demo", stdout=StringIO())
self.assertIn("prod", str(ctx.exception).lower())
def test_seeds_shop_and_portal_rows(self):
out = StringIO()
call_command("seed_demo", stdout=out)
self.assertIn("Demo seed ready", out.getvalue())
from contacts.models import Contact
from leads.models import Lead
from shop.models import Order, Product
self.assertTrue(Product.objects.filter(sku="DEMO-NOTEBOOK").exists())
self.assertTrue(Product.objects.filter(sku="DEMO-PROTO", is_published=False).exists())
self.assertGreaterEqual(Contact.objects.filter(email__startswith="demo+").count(), 8)
self.assertTrue(Lead.objects.filter(message__startswith="[demo-seed]").exists())
self.assertTrue(Order.objects.filter(number="DEMO-ORD-001", status="paid").exists())
listing = Client().get(reverse("shop:list"))
self.assertContains(listing, "Notebook")
from django.contrib.auth import get_user_model
from shop.models import ProductReview
from shipping.models import Shipment
User = get_user_model()
jordan = User.objects.get(username="demo+jordan@example.com")
self.assertFalse(jordan.is_staff)
self.assertTrue(jordan.check_password("Demo-buyer-pass-123"))
self.assertEqual(
Order.objects.get(number="DEMO-ORD-001").user_id, jordan.pk
)
self.assertIsNone(Order.objects.get(number="DEMO-ORD-004").user_id)
self.assertTrue(
ProductReview.objects.filter(
user=jordan, product__sku="DEMO-NOTEBOOK", rating=5
).exists()
)
delivered = Shipment.objects.get(order__number="DEMO-ORD-002")
self.assertEqual(delivered.tracking_status, "delivered")
self.assertGreaterEqual(len(delivered.tracking_events), 2)
transit = Shipment.objects.get(order__number="DEMO-ORD-001")
self.assertEqual(transit.tracking_status, "in_transit")
client = Client()
self.assertTrue(
client.login(
username="demo+jordan@example.com", password="Demo-buyer-pass-123"
)
)
history = client.get(reverse("account:orders"))
self.assertContains(history, "DEMO-ORD-001")
self.assertNotContains(history, "DEMO-ORD-004")
detail = client.get(reverse("shop:detail", kwargs={"slug": "demo-notebook"}))
self.assertContains(detail, "Solid notebook")
def test_second_run_is_idempotent(self):
call_command("seed_demo", stdout=StringIO())
from shop.models import Product
count = Product.objects.filter(sku__startswith="DEMO-").count()
call_command("seed_demo", stdout=StringIO())
self.assertEqual(Product.objects.filter(sku__startswith="DEMO-").count(), count)
def test_reset_rebuilds_tagged_rows(self):
call_command("seed_demo", stdout=StringIO())
from shop.models import Product
Product.objects.filter(sku="DEMO-NOTEBOOK").delete()
call_command("seed_demo", reset=True, stdout=StringIO())
self.assertTrue(Product.objects.filter(sku="DEMO-NOTEBOOK").exists())
from django.contrib.auth import get_user_model
self.assertTrue(
get_user_model().objects.filter(username="demo+jordan@example.com").exists()
)
@patch.dict(os.environ, {"DJANGO_ENV": "beta"})
def test_allows_beta(self):
call_command("seed_demo", stdout=StringIO())
from shop.models import Product
self.assertTrue(Product.objects.filter(sku__startswith="DEMO-").exists())
def test_succeeds_when_shop_not_installed(self):
from django.apps import apps
from contacts.models import Contact
real = apps.is_installed
def fake(name):
if name in {"shop", "shipping"}:
return False
return real(name)
with patch("django.apps.apps.is_installed", side_effect=fake):
call_command("seed_demo", stdout=StringIO())
self.assertTrue(Contact.objects.filter(email__startswith="demo+").exists())
+1 -2
View File
@@ -111,8 +111,7 @@ class CampaignShortLinkViewTests(TestCase):
User = get_user_model()
self.user = User.objects.create_user(
username="utm-composer", password="test-pass-123"
)
username="utm-composer", password="test-pass-123", is_staff=True)
self.client.login(username="utm-composer", password="test-pass-123")
def test_requires_login(self):
+2
View File
@@ -122,9 +122,11 @@ class InstalledOptionalAppsTests(TestCase):
self.assertTrue(reverse("social:composer").startswith("/portal/social/"))
self.assertTrue(reverse("social_ai:generate").startswith("/portal/social/api/generate/"))
self.assertTrue(reverse("shop:list").startswith("/shop/"))
self.assertTrue(reverse("account:login").startswith("/account/"))
self.assertTrue(reverse("shop_portal:product_list").startswith("/portal/shop/"))
self.assertTrue(reverse("shop_portal:sales").startswith("/portal/shop/sales/"))
self.assertTrue(reverse("pos_sync:event_list").startswith("/portal/pos/"))
self.assertTrue(reverse("events:list").startswith("/events/"))
self.assertTrue(reverse("events_portal:event_list").startswith("/portal/events/"))
self.assertTrue(reverse("shipping:shipment_list").startswith("/portal/shipping/"))
self.assertTrue(reverse("shipping:easypost_webhook").startswith("/portal/shipping/webhooks/"))