Template
Add shopper accounts, reviews, tracking, and seed_demo (#9)
Closes #9. Shop-gated buyer accounts, purchase reviews, Stripe customer ids, shipment tracking, slim public contact form, and a template-neutral seed_demo command.
This commit is contained in:
+138
-5
@@ -4,6 +4,7 @@ from decimal import Decimal, InvalidOperation
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Avg, Count
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@@ -12,7 +13,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
|
||||
from shop.models import Order, Product, ProductReview
|
||||
from shop.services import (
|
||||
ShopError,
|
||||
add_to_cart,
|
||||
@@ -23,8 +24,10 @@ from shop.services import (
|
||||
create_checkout_session,
|
||||
create_order_from_cart,
|
||||
mark_paid,
|
||||
qualifying_order_for_review,
|
||||
save_cart,
|
||||
set_cart_qty,
|
||||
user_has_reviewed,
|
||||
)
|
||||
from shop.stats import sales_dashboard
|
||||
|
||||
@@ -45,13 +48,63 @@ def product_list(request):
|
||||
|
||||
def product_detail(request, slug):
|
||||
product = get_object_or_404(Product, slug=slug, is_published=True)
|
||||
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",
|
||||
{"product": product, "available": available_qty(product)},
|
||||
{
|
||||
"product": product,
|
||||
"available": available_qty(product),
|
||||
"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(
|
||||
@@ -104,12 +157,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})
|
||||
@@ -127,10 +186,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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -236,10 +313,62 @@ 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)
|
||||
qs = Order.objects.prefetch_related("items")
|
||||
from django.apps import apps as django_apps
|
||||
|
||||
if django_apps.is_installed("shipping"):
|
||||
qs = qs.prefetch_related("shipments")
|
||||
order = get_object_or_404(qs, 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
|
||||
from django.apps import apps as django_apps
|
||||
|
||||
claim_orders_for_user(request.user)
|
||||
qs = Order.objects.filter(user=request.user).prefetch_related("items")
|
||||
if django_apps.is_installed("shipping"):
|
||||
qs = qs.prefetch_related("shipments")
|
||||
orders = qs.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):
|
||||
from django.apps import apps as django_apps
|
||||
|
||||
qs = Order.objects.prefetch_related("items")
|
||||
if django_apps.is_installed("shipping"):
|
||||
qs = qs.prefetch_related("shipments")
|
||||
order = get_object_or_404(qs, pk=pk, user=request.user)
|
||||
|
||||
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):
|
||||
@@ -269,6 +398,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