bornhack-website/src/tickets/models.py

225 lines
6.5 KiB
Python

import base64
import hashlib
import io
import logging
from decimal import Decimal
from typing import Union
import qrcode
from django.conf import settings
from django.db import models
from django.db.models import (
Count,
Expression,
ExpressionWrapper,
F,
OuterRef,
Subquery,
Sum,
)
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from utils.models import CampRelatedModel, UUIDModel
from utils.pdf import generate_pdf_letter
logger = logging.getLogger("bornhack.%s" % __name__)
class TicketTypeQuerySet(models.QuerySet):
def with_price_stats(self):
def _make_subquery(annotation: Union[Expression, F]) -> Subquery:
return Subquery(
TicketType.objects.annotate(annotation_value=annotation)
.filter(pk=OuterRef("pk"))
.values("annotation_value")[:1]
)
quantity = F("product__orderproductrelation__quantity")
cost = quantity * F("product__cost")
income = quantity * F("product__price")
avg_ticket_price = Subquery(
TicketType.objects.annotate(units=Sum(quantity))
.annotate(income=Sum(income))
.annotate(
avg_ticket_price=ExpressionWrapper(
F("income") * Decimal("1.0") / F("units"),
output_field=models.DecimalField(),
)
)
.filter(pk=OuterRef("pk"))
.values("avg_ticket_price")[:1],
output_field=models.DecimalField(),
)
return self.annotate(
shopticket_count=_make_subquery(Count("shopticket")),
total_units_sold=_make_subquery(quantity),
total_income=_make_subquery(income),
total_cost=_make_subquery(cost),
total_profit=_make_subquery(income - cost),
avg_ticket_price=avg_ticket_price,
).distinct()
# TicketType can be full week, one day, cabins, parking, merch, hax, massage, etc.
class TicketType(CampRelatedModel, UUIDModel):
objects = TicketTypeQuerySet.as_manager()
name = models.TextField()
camp = models.ForeignKey("camps.Camp", on_delete=models.PROTECT)
includes_badge = models.BooleanField(default=False)
single_ticket_per_product = models.BooleanField(
default=False,
help_text=(
"Only create one ticket for a product/order pair no matter the quantity. "
"Useful for products which are bought in larger quantity (ie. village chairs)"
),
)
def __str__(self):
return "{} ({})".format(self.name, self.camp.title)
def create_ticket_token(string):
return hashlib.sha256(string).hexdigest()
def qr_code_base64(token):
qr = qrcode.make(
token, version=1, error_correction=qrcode.constants.ERROR_CORRECT_H
).resize((250, 250))
file_like = io.BytesIO()
qr.save(file_like, format="png")
qrcode_base64 = base64.b64encode(file_like.getvalue())
return qrcode_base64
class BaseTicket(CampRelatedModel, UUIDModel):
ticket_type = models.ForeignKey("TicketType", on_delete=models.PROTECT)
used = models.BooleanField(default=False)
used_time = models.DateTimeField(null=True)
badge_handed_out = models.BooleanField(default=False)
token = models.CharField(max_length=64, blank=True)
badge_token = models.CharField(max_length=64, blank=True)
class Meta:
abstract = True
camp_filter = "ticket_type__camp"
@property
def camp(self):
return self.ticket_type.camp
def save(self, **kwargs):
self.token = self._get_token()
self.badge_token = self._get_badge_token()
super().save(**kwargs)
def _get_token(self):
return create_ticket_token(
"{_id}{secret_key}".format(
_id=self.uuid, secret_key=settings.SECRET_KEY
).encode("utf-8")
)
def _get_badge_token(self):
return create_ticket_token(
"{_id}{secret_key}-badge".format(
_id=self.uuid, secret_key=settings.SECRET_KEY
).encode("utf-8")
)
def get_qr_code_url(self):
return "data:image/png;base64,{}".format(
qr_code_base64(self._get_token()).decode("utf-8")
)
def get_qr_badge_code_url(self):
return "data:image/png;base64,{}".format(
qr_code_base64(self._get_badge_token()).decode("utf-8")
)
def generate_pdf(self):
formatdict = {"ticket": self}
if self.ticket_type.single_ticket_per_product and self.shortname == "shop":
formatdict["quantity"] = self.opr.quantity
return generate_pdf_letter(
filename="{}_ticket_{}.pdf".format(self.shortname, self.pk),
formatdict=formatdict,
template="pdf/ticket.html",
)
class SponsorTicket(BaseTicket):
sponsor = models.ForeignKey("sponsors.Sponsor", on_delete=models.PROTECT)
def __str__(self):
return "SponsorTicket: {}".format(self.pk)
@property
def shortname(self):
return "sponsor"
class DiscountTicket(BaseTicket):
price = models.IntegerField(
help_text=_("Price of the discounted ticket (in DKK, including VAT).")
)
def __str__(self):
return "DiscountTicket: {}".format(self.pk)
@property
def shortname(self):
return "discount"
class ShopTicket(BaseTicket):
opr = models.ForeignKey(
"shop.OrderProductRelation",
related_name="shoptickets",
on_delete=models.PROTECT,
)
product = models.ForeignKey("shop.Product", on_delete=models.PROTECT)
name = models.CharField(
max_length=100,
help_text=(
"Name of the person this ticket belongs to. "
"This can be different from the buying user."
),
null=True,
blank=True,
)
email = models.EmailField(null=True, blank=True)
# overwrite the _get_token method because old tickets use the user_id
def _get_token(self):
return hashlib.sha256(
"{_id}{user_id}{secret_key}".format(
_id=self.pk, user_id=self.order.user.pk, secret_key=settings.SECRET_KEY
).encode("utf-8")
).hexdigest()
def __str__(self):
return "Ticket {user} {product}".format(
user=self.order.user, product=self.product
)
def get_absolute_url(self):
return str(reverse_lazy("tickets:shopticket_edit", kwargs={"pk": self.pk}))
@property
def shortname(self):
return "shop"
@property
def order(self):
return self.opr.order