generated from westfarn/web_django_template
Add customer accounts, shipment tracking, and purchase reviews.
CI / test (pull_request) Successful in 35s
CI / test (pull_request) Successful in 35s
Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
This commit is contained in:
+131
-5
@@ -5,7 +5,7 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Avg, Count, Prefetch
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@@ -14,7 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods, require_POST
|
||||
|
||||
from contacts.models import Contact
|
||||
from shop.models import Order, Product, ProductColor, ProductImage
|
||||
from shop.models import Order, Product, ProductColor, ProductImage, ProductReview
|
||||
from shop.services import (
|
||||
ShopError,
|
||||
add_to_cart,
|
||||
@@ -27,12 +27,14 @@ from shop.services import (
|
||||
create_order_from_cart,
|
||||
mark_paid,
|
||||
product_media_payload,
|
||||
qualifying_order_for_review,
|
||||
refresh_listing_image,
|
||||
remove_product_images,
|
||||
save_cart,
|
||||
set_cart_qty,
|
||||
store_product_stl,
|
||||
sync_product_colors,
|
||||
user_has_reviewed,
|
||||
)
|
||||
from shop.stats import sales_dashboard
|
||||
|
||||
@@ -85,6 +87,16 @@ def product_detail(request, slug):
|
||||
)
|
||||
colors = list(product.colors.all())
|
||||
selected = colors[0] if colors else None
|
||||
reviews = list(product.reviews.select_related("user").all()[:50])
|
||||
stats = product.reviews.aggregate(avg=Avg("rating"), n=Count("id"))
|
||||
can_review = False
|
||||
already_reviewed = False
|
||||
if request.user.is_authenticated:
|
||||
already_reviewed = user_has_reviewed(request.user, product)
|
||||
can_review = (
|
||||
not already_reviewed
|
||||
and qualifying_order_for_review(request.user, product) is not None
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"shop/detail.html",
|
||||
@@ -96,10 +108,47 @@ def product_detail(request, slug):
|
||||
"gallery_items": product.gallery_items(selected),
|
||||
"gallery_photos": product.photos_for(selected),
|
||||
"gallery_data": product_media_payload(product, colors),
|
||||
"reviews": reviews,
|
||||
"review_avg": stats["avg"],
|
||||
"review_count": stats["n"] or 0,
|
||||
"can_review": can_review,
|
||||
"already_reviewed": already_reviewed,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required(login_url="account:login")
|
||||
@require_POST
|
||||
def product_review(request, slug):
|
||||
product = get_object_or_404(Product, slug=slug, is_published=True)
|
||||
if user_has_reviewed(request.user, product):
|
||||
messages.info(request, "You already reviewed this product.")
|
||||
return redirect("shop:detail", slug=product.slug)
|
||||
order = qualifying_order_for_review(request.user, product)
|
||||
if order is None:
|
||||
messages.error(request, "Only customers who purchased this product can review it.")
|
||||
return redirect("shop:detail", slug=product.slug)
|
||||
try:
|
||||
rating = int(request.POST.get("rating") or "0")
|
||||
except ValueError:
|
||||
rating = 0
|
||||
if rating < 1 or rating > 5:
|
||||
messages.error(request, "Choose a rating from 1 to 5.")
|
||||
return redirect("shop:detail", slug=product.slug)
|
||||
title = (request.POST.get("title") or "").strip()[:120]
|
||||
body = (request.POST.get("body") or "").strip()
|
||||
ProductReview.objects.create(
|
||||
product=product,
|
||||
user=request.user,
|
||||
order=order,
|
||||
rating=rating,
|
||||
title=title,
|
||||
body=body,
|
||||
)
|
||||
messages.success(request, "Thanks for the review.")
|
||||
return redirect("shop:detail", slug=product.slug)
|
||||
|
||||
|
||||
def cart_view(request):
|
||||
lines = cart_lines(request.session)
|
||||
return render(
|
||||
@@ -169,12 +218,18 @@ def checkout(request):
|
||||
state=request.POST.get("address_state") or "",
|
||||
zip_code=request.POST.get("address_zip") or "",
|
||||
)
|
||||
buyer = request.user if request.user.is_authenticated else None
|
||||
if buyer:
|
||||
email = (buyer.email or buyer.username or email).strip()
|
||||
if not name:
|
||||
name = buyer.get_full_name()
|
||||
try:
|
||||
order = create_order_from_cart(
|
||||
request.session,
|
||||
email=email,
|
||||
customer_name=name,
|
||||
shipping_address=address,
|
||||
user=buyer,
|
||||
)
|
||||
base = _site_base(request)
|
||||
success = base + reverse("shop:checkout_success", kwargs={"pk": order.pk})
|
||||
@@ -192,10 +247,28 @@ def checkout(request):
|
||||
else:
|
||||
save_cart(request.session, {})
|
||||
return redirect(url)
|
||||
checkout_initial = {
|
||||
"email": "",
|
||||
"customer_name": "",
|
||||
"address": {},
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
from accounts.services import get_customer_profile
|
||||
|
||||
profile = get_customer_profile(request.user)
|
||||
checkout_initial = {
|
||||
"email": request.user.email or request.user.username,
|
||||
"customer_name": request.user.get_full_name(),
|
||||
"address": profile.shipping_address or {},
|
||||
}
|
||||
return render(
|
||||
request,
|
||||
"shop/checkout.html",
|
||||
{"lines": lines, "total": cart_total(lines)},
|
||||
{
|
||||
"lines": lines,
|
||||
"total": cart_total(lines),
|
||||
"checkout_initial": checkout_initial,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -361,10 +434,59 @@ def portal_order_list(request):
|
||||
|
||||
@login_required
|
||||
def portal_order_detail(request, pk):
|
||||
order = get_object_or_404(Order.objects.prefetch_related("items"), pk=pk)
|
||||
order = get_object_or_404(
|
||||
Order.objects.prefetch_related("items", "shipments"), pk=pk
|
||||
)
|
||||
return render(request, "shop/portal/order_detail.html", {"order": order})
|
||||
|
||||
|
||||
@login_required(login_url="account:login")
|
||||
def account_order_list(request):
|
||||
from accounts.services import claim_orders_for_user
|
||||
|
||||
claim_orders_for_user(request.user)
|
||||
orders = (
|
||||
Order.objects.filter(user=request.user)
|
||||
.prefetch_related("items", "shipments")
|
||||
.exclude(status=Order.Status.DRAFT)
|
||||
)
|
||||
return render(request, "shop/account/orders.html", {"orders": orders})
|
||||
|
||||
|
||||
@login_required(login_url="account:login")
|
||||
def account_order_detail(request, pk):
|
||||
order = get_object_or_404(
|
||||
Order.objects.prefetch_related("items", "shipments"),
|
||||
pk=pk,
|
||||
user=request.user,
|
||||
)
|
||||
from django.apps import apps as django_apps
|
||||
|
||||
if django_apps.is_installed("shipping"):
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
from shipping.models import Shipment
|
||||
from shipping.services import refresh_tracking
|
||||
|
||||
stale_after = timezone.now() - timedelta(minutes=15)
|
||||
for shipment in order.shipments.all():
|
||||
if (
|
||||
shipment.status == Shipment.Status.LABELED
|
||||
and shipment.tracking_number
|
||||
and shipment.tracking_status != Shipment.TrackingStatus.DELIVERED
|
||||
and (
|
||||
shipment.last_tracked_at is None
|
||||
or shipment.last_tracked_at < stale_after
|
||||
)
|
||||
):
|
||||
try:
|
||||
refresh_tracking(shipment)
|
||||
except Exception:
|
||||
logger.exception("order tracking refresh failed for %s", order.number)
|
||||
return render(request, "shop/account/order_detail.html", {"order": order})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def stripe_webhook(request):
|
||||
@@ -394,6 +516,10 @@ def stripe_webhook(request):
|
||||
session_id = obj.get("id") or ""
|
||||
order = Order.objects.filter(stripe_checkout_session_id=session_id).first()
|
||||
if order and order.status != Order.Status.PAID:
|
||||
mark_paid(order, stripe_id=obj.get("id") or "")
|
||||
mark_paid(
|
||||
order,
|
||||
stripe_id=obj.get("id") or "",
|
||||
stripe_customer_id=obj.get("customer") or "",
|
||||
)
|
||||
logger.info("shop order %s marked paid", order.number)
|
||||
return HttpResponse("ok")
|
||||
|
||||
Reference in New Issue
Block a user