generated from westfarn/web_django_template
Ship Print Forge shop site (colors, photos, 3D viewer) (#2)
## 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:
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user