Design Review System¶
This document describes the hybrid design review workflow for custom designs.
Overview¶
The design review system uses a hybrid approach: - Auto-approve: Designs containing only text and clipart are automatically finalized - Manual review: Designs with user-uploaded images require staff review for print quality
This balances customer experience (fast turnaround for simple designs) with quality control (uploaded images need resolution/quality checks).
Implementation status (verified 2026-07-02)
The review flow is now driven by explicit actions rather than automatic upload-detection:
- Customer requests review:
POST /api/designs/{id}/request_approval/moves adraftorrejecteddesign topending_review(CustomDesignViewSet.request_approval,backend/apps/designs/views.py). - Admin decides:
POST /api/admin/designs/{id}/approve/(pending_review→approved, unblocks checkout) orPOST /api/admin/designs/{id}/reject/(pending_review→rejected, requires areason, stored inreview_note). Both stampreviewed_at/reviewed_byand write an audit log (backend/apps/designs/admin_views.py). - Finalize (
PATCH /api/designs/{id}/finalize/) is a separate "lock for production" action that setsfinalizedunconditionally. - The upload-detection helper
CustomDesign.has_user_uploaded_images()still exists on the model but is not wired into an automatic pending-review trigger — moving topending_reviewis an explicit customer/admin action. - Checkout is blocked while the cart contains a
pending_review(orrejected) design (OrderService._validate_design_statusinbackend/apps/orders/services.py).
Design Status Workflow¶
stateDiagram-v2
[*] --> draft
draft --> pending_review: request_approval
rejected --> pending_review: request_approval
pending_review --> approved: admin approve
pending_review --> rejected: admin reject (reason)
approved --> finalized: finalize (lock)
draft --> finalized: finalize (lock)
finalized --> in_production: order fulfilment
| Status | Description | When Used |
|---|---|---|
draft |
Design is being edited | Default status, active editing |
pending_review |
Awaiting staff review | Customer requested approval (request_approval) |
approved |
Passed staff review | Admin approve — unblocks checkout |
rejected |
Rejected by staff | Admin reject with a reason (shown to customer) |
finalized |
Locked for production | finalize action |
in_production |
Being manufactured | Order is being fulfilled |
Email Notifications¶
Design Pending Review¶
Trigger: Design status changes to pending_review
Template: design_pending_review.html / .txt
Subject: Je ontwerp voor {product_name} wordt beoordeeld
Content: - Explains uploaded images are being checked for print quality - Sets expectation: review within 24 hours - Preview thumbnail of the design - Link to view/edit the design
Design Approved¶
Trigger: Design status changes to approved or finalized
Template: design_approved.html / .txt
Subject: Je ontwerp voor {product_name} is goedgekeurd!
Content: - Confirms design is approved and ready for production - Preview thumbnail - Link to designer or cart
Design Rejected¶
Trigger: Design status changes to rejected
Template: design_rejected.html / .txt
Content:
- Explains the design was not approved, including the rejection reason
- Link back to the designer so the customer can adjust and re-request approval
Status-change emails are dispatched from backend/apps/designs/signals.py
(via transaction.on_commit) using the model's FieldTracker.
Email Recipient Logic¶
The system uses a fallback pattern for email recipients:
def get_recipient_email(design):
# 1. Prefer user's account email
if design.user:
return design.user.email
# 2. Fallback to order email for guest checkout
order_item = design.orderitem_set.select_related('order').first()
if order_item and order_item.order:
return order_item.order.email
return None
This ensures guests who checkout without an account still receive design status emails.
Detecting User Uploads¶
The has_user_uploaded_images() method checks if a design contains user-uploaded images:
def has_user_uploaded_images(self):
for view in ["front", "back", "left", "right"]:
view_json = getattr(self, f"design_json_{view}", {})
for obj in view_json.get("objects", []):
if obj.get("type") in ("image", "Image"):
src = obj.get("src", "")
# User uploads in /uploads/, clipart in /clipart/
if "/uploads/" in src or src.startswith("data:"):
return True
return False
Detection logic:
- User uploads are stored in /uploads/ directory
- Clipart is stored in /clipart/ directory
- Base64 data URLs (data:) are treated as user uploads
Frontend Components¶
ImageTab Info Banner¶
Location: frontend/components/ProductDesigner/components/tabs/ImageTab.tsx
A blue info banner warns users that uploaded images require review:
Let op: Geuploade afbeeldingen worden door ons team gecontroleerd op printkwaliteit. Je ontvangt binnen 24 uur een e-mail zodra je ontwerp is goedgekeurd.
Order Progress Stepper¶
Location: frontend/components/orders/OrderProgressStepper.tsx
Shows order fulfillment progress:
Also displays:
- Design review notice when status is pending_review
- Waiting for payment notice when status is pending
Design Status Badges¶
On the order detail page, each order item shows a design status badge: - 🔍 Ontwerp wordt beoordeeld - pending_review - ✓ Ontwerp goedgekeurd - finalized
Admin Workflow¶
The primary review UI is the frontend admin (/admin), backed by the
/api/admin/designs/{id}/approve/ and /reject/ actions:
- Open the admin designs list and filter by status
pending_review - Review the design preview images and uploaded-image quality/resolution
- Approve (
pending_review→approved) — unblocks checkout, or Reject (pending_review→rejected) with a reason shown to the customer - A status email is sent automatically (see the signals below)
The Django admin can still be used for ad-hoc status changes (status is
editable there), but approve/reject with audit + review metadata is the
intended path.
API Endpoints¶
| Method & path | Actor | Effect |
|---|---|---|
POST /api/designs/{id}/request_approval/ |
Customer | draft/rejected → pending_review |
POST /api/admin/designs/{id}/approve/ |
Staff | pending_review → approved (unblocks checkout) |
POST /api/admin/designs/{id}/reject/ |
Staff | pending_review → rejected; body {"reason": "..."} required |
PATCH /api/designs/{id}/finalize/ |
Customer/system | → finalized (locks the design) |
The generic PATCH /api/designs/{id}/ still exists; status is a writable
serializer field, so it can also be set directly (e.g. from the Django admin).
Database¶
The current STATUS_CHOICES on CustomDesign (backend/apps/designs/models.py):
STATUS_CHOICES = [
("draft", "Draft"),
("pending_review", "Pending Review"),
("approved", "Approved"),
("rejected", "Rejected"),
("finalized", "Finalized"),
("in_production", "In Production"),
]
The model also carries review metadata (review_note, reviewed_at,
reviewed_by) populated by the admin approve/reject actions.
Testing¶
Backend Tests¶
Frontend Tests¶
Manual E2E Test¶
- Upload image in designer → see info banner
- Move the design to
pending_review(customerrequest_approval, Django admin, or API PATCH) → checkout with this design in the cart is blocked with a notice - Check pending-review email received (user email, or order email for guests)
- Admin: approve the design (
pending_review→approved) - Check approval email received; checkout is possible again
- (Alternatively) Admin reject with a reason → rejected email is sent and checkout stays blocked until the customer re-requests and it's approved
- Order detail page shows progress stepper
Configuration¶
No additional configuration is required. The feature uses existing environment variables:
| Variable | Purpose |
|---|---|
FRONTEND_URL |
Base URL for designer links in emails |
| Email settings | Existing Anymail/SMTP configuration |