generated from westfarn/web_django_template
Replaces the client_site template branding, adds shop/shipping, and points beta CI at master for easy deploy. Closes #1
112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
from decimal import Decimal
|
|
|
|
from django.db import models
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.text import slugify
|
|
|
|
from core.models import TimeStampedModel, UUIDPrimaryKeyModel
|
|
|
|
|
|
class Event(UUIDPrimaryKeyModel, TimeStampedModel):
|
|
title = models.CharField(max_length=200)
|
|
slug = models.SlugField(max_length=220, unique=True)
|
|
description = models.TextField(blank=True)
|
|
starts_at = models.DateTimeField()
|
|
venue = models.CharField(max_length=200, blank=True)
|
|
capacity = models.PositiveIntegerField(default=0)
|
|
price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0"))
|
|
currency = models.CharField(max_length=8, default="usd")
|
|
is_published = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
ordering = ["starts_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return self.title
|
|
|
|
def get_absolute_url(self) -> str:
|
|
return reverse("events:detail", kwargs={"slug": self.slug})
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.slug:
|
|
base = slugify(self.title)[:200] or "event"
|
|
slug = base
|
|
n = 2
|
|
while Event.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
|
slug = f"{base}-{n}"
|
|
n += 1
|
|
self.slug = slug
|
|
super().save(*args, **kwargs)
|
|
|
|
def tickets_held(self, *, exclude_order_id=None) -> int:
|
|
qs = self.ticket_orders.filter(
|
|
status__in=[
|
|
TicketOrder.Status.DRAFT,
|
|
TicketOrder.Status.OPEN,
|
|
TicketOrder.Status.PAID,
|
|
]
|
|
)
|
|
if exclude_order_id:
|
|
qs = qs.exclude(pk=exclude_order_id)
|
|
return qs.aggregate(total=models.Sum("quantity"))["total"] or 0
|
|
|
|
@property
|
|
def tickets_sold(self) -> int:
|
|
return self.tickets_held()
|
|
|
|
@property
|
|
def seats_remaining(self) -> int | None:
|
|
if not self.capacity:
|
|
return None
|
|
return max(self.capacity - self.tickets_sold, 0)
|
|
|
|
@property
|
|
def is_upcoming(self) -> bool:
|
|
return self.starts_at >= timezone.now()
|
|
|
|
|
|
class TicketOrder(UUIDPrimaryKeyModel, TimeStampedModel):
|
|
class Status(models.TextChoices):
|
|
DRAFT = "draft", "Draft"
|
|
OPEN = "open", "Open"
|
|
PAID = "paid", "Paid"
|
|
CANCELLED = "cancelled", "Cancelled"
|
|
|
|
number = models.CharField(max_length=32, unique=True)
|
|
event = models.ForeignKey(
|
|
Event, on_delete=models.PROTECT, related_name="ticket_orders"
|
|
)
|
|
email = models.EmailField()
|
|
customer_name = models.CharField(max_length=200, blank=True)
|
|
quantity = models.PositiveIntegerField(default=1)
|
|
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
|
currency = models.CharField(max_length=8, default="usd")
|
|
status = models.CharField(
|
|
max_length=16, choices=Status.choices, default=Status.DRAFT
|
|
)
|
|
stripe_checkout_session_id = models.CharField(max_length=255, blank=True)
|
|
hosted_checkout_url = models.URLField(blank=True)
|
|
paid_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.number} · {self.event}"
|
|
|
|
|
|
class Ticket(UUIDPrimaryKeyModel, TimeStampedModel):
|
|
order = models.ForeignKey(
|
|
TicketOrder, on_delete=models.CASCADE, related_name="tickets"
|
|
)
|
|
event = models.ForeignKey(Event, on_delete=models.PROTECT, related_name="tickets")
|
|
code = models.CharField(max_length=16, unique=True)
|
|
attendee_name = models.CharField(max_length=200, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ["created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return self.code
|