generated from westfarn/web_django_template
Stand up Print Forge as a 3D-printed toy shop with color variants and printer photography.
Replaces the client_site template branding, adds shop/shipping, and points beta CI at master for easy deploy. Closes #1
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import logging
|
||||
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.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from datetime import datetime
|
||||
from django.utils.text import slugify
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from events.models import Event, TicketOrder
|
||||
from events.services import (
|
||||
EventsError,
|
||||
create_checkout_session,
|
||||
create_ticket_order,
|
||||
mark_paid,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _site_base(request) -> str:
|
||||
base = (settings.PUBLIC_SITE_URL or "").rstrip("/")
|
||||
if base:
|
||||
return base
|
||||
return request.build_absolute_uri("/").rstrip("/")
|
||||
|
||||
|
||||
def event_list(request):
|
||||
events = Event.objects.filter(is_published=True, starts_at__gte=timezone.now())
|
||||
return render(request, "events/list.html", {"events": events})
|
||||
|
||||
|
||||
def event_detail(request, slug):
|
||||
event = get_object_or_404(Event, slug=slug, is_published=True)
|
||||
return render(request, "events/detail.html", {"event": event})
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def buy_tickets(request, slug):
|
||||
event = get_object_or_404(Event, slug=slug, is_published=True)
|
||||
if request.method == "POST":
|
||||
email = (request.POST.get("email") or "").strip()
|
||||
name = (request.POST.get("customer_name") or "").strip()
|
||||
try:
|
||||
quantity = int(request.POST.get("quantity") or "1")
|
||||
except ValueError:
|
||||
quantity = 0
|
||||
try:
|
||||
order = create_ticket_order(
|
||||
event, email=email, customer_name=name, quantity=quantity
|
||||
)
|
||||
base = _site_base(request)
|
||||
success = base + reverse(
|
||||
"events:checkout_success", kwargs={"pk": order.pk}
|
||||
)
|
||||
cancel = base + reverse("events:checkout_cancel", kwargs={"pk": order.pk})
|
||||
url = create_checkout_session(
|
||||
order,
|
||||
success_url=success + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url=cancel,
|
||||
)
|
||||
except EventsError as exc:
|
||||
messages.error(request, str(exc))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception("ticket checkout failed")
|
||||
messages.error(request, f"Could not start checkout: {exc}")
|
||||
else:
|
||||
return redirect(url)
|
||||
return render(request, "events/buy.html", {"event": event})
|
||||
|
||||
|
||||
def checkout_success(request, pk):
|
||||
order = get_object_or_404(TicketOrder.objects.select_related("event"), pk=pk)
|
||||
return render(request, "events/success.html", {"order": order})
|
||||
|
||||
|
||||
def checkout_cancel(request, pk):
|
||||
order = get_object_or_404(TicketOrder.objects.select_related("event"), pk=pk)
|
||||
return render(request, "events/cancel.html", {"order": order})
|
||||
|
||||
|
||||
@login_required
|
||||
def portal_event_list(request):
|
||||
events = Event.objects.all().order_by("-starts_at")
|
||||
return render(request, "events/portal/list.html", {"events": events})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def portal_event_edit(request, pk=None):
|
||||
event = get_object_or_404(Event, pk=pk) if pk else None
|
||||
if request.method == "POST":
|
||||
title = (request.POST.get("title") or "").strip()
|
||||
description = (request.POST.get("description") or "").strip()
|
||||
venue = (request.POST.get("venue") or "").strip()
|
||||
slug = (request.POST.get("slug") or "").strip()
|
||||
starts_raw = (request.POST.get("starts_at") or "").strip()
|
||||
errors = []
|
||||
if not title:
|
||||
errors.append("Title is required.")
|
||||
starts_at = None
|
||||
if starts_raw:
|
||||
try:
|
||||
starts_at = datetime.fromisoformat(starts_raw)
|
||||
except ValueError:
|
||||
starts_at = None
|
||||
if starts_at is None:
|
||||
errors.append("Start date/time is required.")
|
||||
elif timezone.is_naive(starts_at):
|
||||
starts_at = timezone.make_aware(starts_at, timezone.get_current_timezone())
|
||||
try:
|
||||
capacity = int(request.POST.get("capacity") or "0")
|
||||
except ValueError:
|
||||
capacity = 0
|
||||
errors.append("Capacity must be a number.")
|
||||
try:
|
||||
price = Decimal(request.POST.get("price") or "0")
|
||||
if price < 0:
|
||||
raise InvalidOperation
|
||||
except Exception:
|
||||
price = None
|
||||
errors.append("Enter a valid price.")
|
||||
if errors:
|
||||
for err in errors:
|
||||
messages.error(request, err)
|
||||
else:
|
||||
if event is None:
|
||||
event = Event()
|
||||
event.title = title
|
||||
event.description = description
|
||||
event.venue = venue
|
||||
event.slug = slugify(slug)[:220] if slug else ""
|
||||
event.starts_at = starts_at
|
||||
event.capacity = max(capacity, 0)
|
||||
event.price = price
|
||||
event.currency = (settings.STRIPE_CURRENCY or "usd").lower()
|
||||
event.is_published = request.POST.get("is_published") == "on"
|
||||
event.save()
|
||||
messages.success(request, f"Saved {event.title}.")
|
||||
return redirect("events_portal:event_list")
|
||||
return render(request, "events/portal/edit.html", {"event": event})
|
||||
|
||||
|
||||
@login_required
|
||||
def portal_event_detail(request, pk):
|
||||
event = get_object_or_404(Event, pk=pk)
|
||||
orders = event.ticket_orders.select_related().prefetch_related("tickets")
|
||||
return render(
|
||||
request,
|
||||
"events/portal/detail.html",
|
||||
{"event": event, "orders": orders},
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def stripe_webhook(request):
|
||||
secret = (settings.STRIPE_WEBHOOK_SECRET or "").strip()
|
||||
if not secret:
|
||||
logger.error("STRIPE_WEBHOOK_SECRET unset")
|
||||
return HttpResponseBadRequest("webhook not configured")
|
||||
try:
|
||||
import stripe
|
||||
except ImportError:
|
||||
return HttpResponseBadRequest("stripe not installed")
|
||||
sig = request.headers.get("Stripe-Signature", "")
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(request.body, sig, secret)
|
||||
except Exception:
|
||||
logger.exception("events stripe webhook signature failed")
|
||||
return HttpResponseBadRequest("invalid signature")
|
||||
|
||||
obj = event.get("data", {}).get("object", {}) or {}
|
||||
if event.get("type") != "checkout.session.completed":
|
||||
return HttpResponse("ok")
|
||||
order_id = (obj.get("metadata") or {}).get("ticket_order_id") or ""
|
||||
order = None
|
||||
if order_id:
|
||||
order = TicketOrder.objects.filter(pk=order_id).first()
|
||||
if order is None:
|
||||
session_id = obj.get("id") or ""
|
||||
order = TicketOrder.objects.filter(
|
||||
stripe_checkout_session_id=session_id
|
||||
).first()
|
||||
if order and order.status != TicketOrder.Status.PAID:
|
||||
mark_paid(order, stripe_id=obj.get("id") or "")
|
||||
logger.info("ticket order %s marked paid", order.number)
|
||||
return HttpResponse("ok")
|
||||
Reference in New Issue
Block a user