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
@@ -0,0 +1,25 @@
{% extends "portal_base.html" %}
{% block title %}{{ shipment.order.number }} · Shipping{% endblock %}
{% block topbar_title %}{{ shipment.order.number }}{% endblock %}
{% block portal_content %}
<p>{{ shipment.order.email }} · {{ shipment.get_status_display }}</p>
{% if shipment.tracking_number %}<p>Tracking: {{ shipment.tracking_number }}</p>{% endif %}
{% if shipment.label_url %}<p><a href="{{ shipment.label_url }}">Download label</a></p>{% endif %}
{% if shipment.status != 'labeled' %}
<form method="post" action="{% url 'shipping:shipment_buy' shipment.pk %}">
{% csrf_token %}
{% for rate in shipment.rates %}
<label>
<input type="radio" name="rate_id" value="{{ rate.id }}" {% if forloop.first %}checked{% endif %}>
{{ rate.carrier }} {{ rate.service }} — {{ rate.rate }} {{ rate.currency }}
</label><br>
{% empty %}
<p>No rates yet.</p>
{% endfor %}
{% if shipment.rates %}
<button class="btn btn-primary" type="submit">Buy label</button>
{% endif %}
</form>
{% endif %}
<p><a href="{% url 'shipping:shipment_list' %}">← All shipments</a></p>
{% endblock %}
@@ -0,0 +1,43 @@
{% extends "portal_base.html" %}
{% block title %}Shipping · Portal{% endblock %}
{% block topbar_title %}Shipping{% endblock %}
{% block portal_content %}
<p><a class="btn btn-ghost" href="{% url 'shipping:pirate_ship_export' %}">Pirate Ship CSV</a></p>
<h2>Unshipped paid orders</h2>
<table class="table">
<thead><tr><th>Order</th><th>Email</th><th></th></tr></thead>
<tbody>
{% for order in unshipped %}
<tr>
<td>{{ order.number }}</td>
<td>{{ order.email }}</td>
<td>
<form method="post" action="{% url 'shipping:shipment_create' %}">
{% csrf_token %}
<input type="hidden" name="order" value="{{ order.pk }}">
<input name="weight_oz" type="number" min="1" value="16" style="width:5rem"> oz
<button class="btn btn-primary" type="submit">Quote rates</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="3" class="empty-state">No paid orders waiting to ship.</td></tr>
{% endfor %}
</tbody>
</table>
<h2>Shipments</h2>
<table class="table">
<thead><tr><th>Order</th><th>Status</th><th>Tracking</th></tr></thead>
<tbody>
{% for shipment in shipments %}
<tr>
<td><a href="{% url 'shipping:shipment_detail' shipment.pk %}">{{ shipment.order.number }}</a></td>
<td>{{ shipment.get_status_display }}</td>
<td>{{ shipment.tracking_number|default:"—" }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="empty-state">No shipments yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}