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
+80
View File
@@ -0,0 +1,80 @@
# Generated by Django 6.1 on 2026-09-06 11:17
import django.db.models.deletion
import uuid
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Order',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('number', models.CharField(max_length=32, unique=True)),
('email', models.EmailField(max_length=254)),
('customer_name', models.CharField(blank=True, max_length=200)),
('status', models.CharField(choices=[('draft', 'Draft'), ('open', 'Open'), ('paid', 'Paid'), ('fulfilled', 'Fulfilled'), ('cancelled', 'Cancelled')], default='draft', max_length=16)),
('amount', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10)),
('currency', models.CharField(default='usd', max_length=8)),
('shipping_address', models.JSONField(blank=True, default=dict)),
('stripe_checkout_session_id', models.CharField(blank=True, max_length=255)),
('hosted_checkout_url', models.URLField(blank=True)),
('paid_at', models.DateTimeField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Product',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('slug', models.SlugField(max_length=220, unique=True)),
('sku', models.CharField(max_length=64, unique=True)),
('description', models.TextField(blank=True)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('currency', models.CharField(default='usd', max_length=8)),
('fulfillment', models.CharField(choices=[('stocked', 'On-hand stock'), ('made_to_order', 'Made to order')], default='stocked', max_length=16)),
('stock_qty', models.IntegerField(default=0)),
('print_minutes', models.PositiveIntegerField(default=0, help_text='Estimated print time per unit (made-to-order).')),
('filament_grams', models.PositiveIntegerField(default=0)),
('is_published', models.BooleanField(default=False)),
('track_inventory', models.BooleanField(default=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='OrderItem',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('sku', models.CharField(max_length=64)),
('quantity', models.PositiveIntegerField(default=1)),
('unit_price', models.DecimalField(decimal_places=2, max_digits=10)),
('print_minutes', models.PositiveIntegerField(default=0)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shop.order')),
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='shop.product')),
],
options={
'ordering': ['created_at'],
},
),
]
@@ -0,0 +1,72 @@
# Generated by Django 6.1
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0002_storedfile_product_image"),
("shop", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="product",
name="image",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="shop_products",
to="core.storedfile",
),
),
migrations.CreateModel(
name="ProductColor",
fields=[
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=64)),
("hex", models.CharField(default="#808080", max_length=7)),
("sort_order", models.PositiveIntegerField(default=0)),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="colors",
to="shop.product",
),
),
],
options={
"ordering": ["sort_order", "name"],
},
),
migrations.AddField(
model_name="orderitem",
name="color",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="order_items",
to="shop.productcolor",
),
),
migrations.AddField(
model_name="orderitem",
name="color_name",
field=models.CharField(blank=True, max_length=64),
),
]
+25
View File
@@ -0,0 +1,25 @@
# Generated by Django 6.1
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0003_storedfile_product_stl"),
("shop", "0002_product_image_and_color"),
]
operations = [
migrations.AddField(
model_name="product",
name="stl",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="shop_product_stls",
to="core.storedfile",
),
),
]
@@ -0,0 +1,97 @@
# Generated by Django 6.1
import uuid
import django.db.models.deletion
from django.db import migrations, models
def copy_listing_image_and_color_stock(apps, schema_editor):
Product = apps.get_model("shop", "Product")
ProductImage = apps.get_model("shop", "ProductImage")
ProductColor = apps.get_model("shop", "ProductColor")
for product in Product.objects.exclude(image_id=None):
ProductImage.objects.create(
product=product,
color=None,
file_id=product.image_id,
sort_order=0,
)
seen = set()
for product in Product.objects.filter(colors__isnull=False).distinct():
if product.pk in seen:
continue
seen.add(product.pk)
first = (
ProductColor.objects.filter(product_id=product.pk)
.order_by("sort_order", "name")
.first()
)
if first and first.stock_qty == 0 and product.stock_qty:
first.stock_qty = product.stock_qty
first.save(update_fields=["stock_qty"])
class Migration(migrations.Migration):
dependencies = [
("core", "0003_storedfile_product_stl"),
("shop", "0003_product_stl"),
]
operations = [
migrations.AddField(
model_name="productcolor",
name="stock_qty",
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name="ProductImage",
fields=[
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("sort_order", models.PositiveIntegerField(default=0)),
(
"color",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="images",
to="shop.productcolor",
),
),
(
"file",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="shop_gallery_images",
to="core.storedfile",
),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="images",
to="shop.product",
),
),
],
options={
"ordering": ["sort_order", "created_at"],
},
),
migrations.RunPython(
copy_listing_image_and_color_stock,
migrations.RunPython.noop,
),
]
View File