Ship Print Forge shop site (colors, photos, 3D viewer) (#2)
Deploy Beta / unit-tests (push) Successful in 31s
Deploy Beta / docker (push) Failing after 34s
Deploy Beta / deploy-beta (push) Skipped

## Summary

- Rebrand the Django client template as Print Forge (`client_site` → `print_forge`) with shop + shipping enabled.
- Product listings support multiple photos, color variants (shared price/description/STL, per-color stock and photos), and a photo-first / 3D-second gallery.
- Public pages use 3D printer / printed-toy photography instead of leftover t-shirt mockups; beta CI deploys on `master`.

Closes #1
Infra: [server-infra#27](ai_ml_operations/server-infra#27) (easy deploy beta).

## Test plan

- [ ] Product page shows photo first, 3D model second; color swatches swap photos and stock
- [ ] Portal can upload multiple photos and per-color qty/images; STL stays shared
- [ ] Public home/about/gallery have no t-shirt mockups
- [ ] `manage.py test` passes
- [ ] After server-infra#27: beta deploy to `print-forge-preview.aimloperations.com`

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-09-06 18:40:42 -07:00
parent 8a97e3fbe2
commit 1ca5a757d9
339 changed files with 9761 additions and 2541 deletions
+95
View File
@@ -0,0 +1,95 @@
from decimal import Decimal
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone
from events.models import Event, Ticket, TicketOrder
from events.services import EventsError, create_ticket_order, mark_paid
def _event(**kwargs):
defaults = dict(
title="Friday Night Prerelease",
starts_at=timezone.now() + timezone.timedelta(days=7),
venue="Main shop",
capacity=8,
price=Decimal("25.00"),
is_published=True,
)
defaults.update(kwargs)
return Event.objects.create(**defaults)
class EventPublicTests(TestCase):
def test_list_hides_drafts_and_past(self):
_event(title="Live prerelease")
_event(title="Draft night", is_published=False)
_event(
title="Last week",
starts_at=timezone.now() - timezone.timedelta(days=2),
)
response = Client().get(reverse("events:list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Live prerelease")
self.assertNotContains(response, "Draft night")
self.assertNotContains(response, "Last week")
def test_buy_form_renders(self):
event = _event()
response = Client().get(reverse("events:buy", kwargs={"slug": event.slug}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Pay with Stripe")
class EventCapacityTests(TestCase):
def test_capacity_blocks_overbook(self):
event = _event(capacity=2)
create_ticket_order(event, email="a@example.com", quantity=2)
with self.assertRaises(EventsError):
create_ticket_order(event, email="b@example.com", quantity=1)
def test_mark_paid_issues_tickets_and_email(self):
event = _event(capacity=4)
order = create_ticket_order(
event, email="fan@example.com", customer_name="Lee", quantity=2
)
mark_paid(order)
order.refresh_from_db()
self.assertEqual(order.status, TicketOrder.Status.PAID)
self.assertEqual(Ticket.objects.filter(order=order).count(), 2)
self.assertEqual(event.tickets_sold, 2)
self.assertEqual(len(mail.outbox), 1)
self.assertIn(event.title, mail.outbox[0].subject)
class EventPortalTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user("host", password="test-pass-123")
self.client = Client()
self.client.login(username="host", password="test-pass-123")
def test_list_requires_login(self):
self.assertEqual(Client().get(reverse("events_portal:event_list")).status_code, 302)
def test_create_published_event(self):
starts = (timezone.now() + timezone.timedelta(days=3)).strftime("%Y-%m-%dT%H:%M")
response = self.client.post(
reverse("events_portal:event_new"),
{
"title": "Commander night",
"starts_at": starts,
"venue": "Back room",
"capacity": "16",
"price": "10.00",
"is_published": "on",
},
)
self.assertEqual(response.status_code, 302)
event = Event.objects.get()
self.assertEqual(event.slug, "commander-night")
self.assertTrue(event.is_published)
self.assertEqual(event.capacity, 16)