Source code for pizzas.models

"""The models defined by the pizzas package"""
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.template.defaulttags import date

from events.models import Event
import members
from members.models import Member
from payments.models import Payment, Payable
from pushnotifications.models import ScheduledMessage, Category
from utils.translation import ModelTranslateMeta, MultilingualField


class PizzaEvent(models.Model):
    """Describes an event where pizzas can be ordered"""

    start = models.DateTimeField(_("Order from"))
    end = models.DateTimeField(_("Order until"))
    event = models.OneToOneField(Event, on_delete=models.CASCADE)

    send_notification = models.BooleanField(
        _("Send an order notification"), default=True
    )
    end_reminder = models.OneToOneField(ScheduledMessage, models.CASCADE, null=True)

    @property
    def title(self):
        return self.event.title

    @property
    def in_the_future(self):
        return self.start > timezone.now()

    @property
    def has_ended(self):
        return self.end < timezone.now()

    @property
    def just_ended(self):
        return (
            self.has_ended and self.end + timezone.timedelta(hours=8) > timezone.now()
        )

    @classmethod
    def current(cls):
        """
        Get the currently relevant pizza event: the first one
        that starts within 8 hours from now.
        """

        try:
            events = PizzaEvent.objects.filter(
                end__gt=timezone.now() - timezone.timedelta(hours=8),
                start__lte=timezone.now() + timezone.timedelta(hours=8),
            ).order_by("start")
            if events.count() > 1:
                return events.exclude(end__lt=timezone.now()).first()
            else:
                return events.get()
        except PizzaEvent.DoesNotExist:
            return None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._end = self.end

    def validate_unique(self, exclude=None):
        super().validate_unique(exclude)
        for other in PizzaEvent.objects.filter(
            Q(end__gte=self.start, end__lte=self.end)
            | Q(start=self.start, start__lte=self.start)
        ):
            if other.pk == self.pk:
                continue
            raise ValidationError(
                {
                    "start": _("This event cannot overlap with {}.").format(other),
                    "end": _("This event cannot overlap with {}.").format(other),
                }
            )

    def clean(self):
        super().clean()

        if self.start >= self.end:
            raise ValidationError(
                {
                    "start": _("The start is after the end of this event."),
                    "end": _("The end is before the start of this event."),
                }
            )

    def save(self, *args, **kwargs):
        if self.send_notification and not self.end_reminder:
            end_reminder = ScheduledMessage()
            end_reminder.title_en = "Order pizza"
            end_reminder.title_nl = "Pizza bestellen"
            end_reminder.body_en = "You can order pizzas for 10 more minutes"
            end_reminder.body_nl = "Je kan nog 10 minuten pizza's bestellen"
            end_reminder.category = Category.objects.get(key=Category.PIZZA)
            end_reminder.time = self.end - timezone.timedelta(minutes=10)
            end_reminder.save()

            if self.event.registration_required:
                end_reminder.users.set(
                    self.event.registrations.select_related("member").values_list(
                        "member", flat=True
                    )
                )
            else:
                end_reminder.users.set(Member.current_members.all())

            self.end_reminder = end_reminder
        elif self.send_notification and self.end_reminder and self._end != self.end:
            self.end_reminder.time = self.end
            self.end_reminder.save()
        elif not self.send_notification and self.end_reminder:
            end_reminder = self.end_reminder
            self.end_reminder = None
            if not end_reminder.sent:
                end_reminder.delete()

        super().save(*args, **kwargs)

    def delete(self, using=None, keep_parents=False):
        if self.end_reminder is not None and not self.end_reminder.sent:
            self.end_reminder.delete()
        return super().delete(using, keep_parents)

    def __str__(self):
        return "Pizzas for " + str(self.event)

    class Meta:
        ordering = ("-start",)


class AvailableProductManager(models.Manager):
    """Only shows available products"""

    def get_queryset(self):
        return super().get_queryset().filter(available=True)


class Product(models.Model, metaclass=ModelTranslateMeta):
    """Describes a product"""

    objects = models.Manager()
    available_products = AvailableProductManager()

    name = models.CharField(max_length=50)
    description = MultilingualField(models.TextField)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    available = models.BooleanField(default=True)
    restricted = models.BooleanField(
        default=False,
        help_text=_(
            "Only allow to be ordered by people with the "
            "'order restricted products' permission."
        ),
    )

    def __str__(self):
        return self.name

    class Meta:
        ordering = ("name",)
        permissions = (("order_restricted_products", _("Order restricted products")),)


class Order(models.Model, Payable):
    """Describes an order of an item during an event"""

    member = models.ForeignKey(
        members.models.Member, on_delete=models.CASCADE, blank=True, null=True,
    )

    name = models.CharField(
        verbose_name=_("name"),
        max_length=50,
        help_text=_("Use this for non-members"),
        null=True,
        blank=True,
    )

    payment = models.OneToOneField(
        verbose_name=_("payment"),
        to="payments.Payment",
        related_name="pizzas_order",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )

    product = models.ForeignKey(
        verbose_name=_("product"), to=Product, on_delete=models.PROTECT,
    )

    pizza_event = models.ForeignKey(
        verbose_name=_("event"), to=PizzaEvent, on_delete=models.CASCADE
    )

    @property
    def payment_amount(self):
        return self.product.price

    @property
    def payment_topic(self):
        start_date = date(self.pizza_event.start, "Y-m-d")
        return f"Pizzas {self.pizza_event.event.title_en} [{start_date}]"

    @property
    def payment_notes(self):
        return (
            f"Pizza order by {self.member_name} "
            f"for {self.pizza_event.event.title_en}"
        )

    @property
    def payment_payer(self):
        return self.member

    def clean(self):
        if (self.member is None and not self.name) or (self.member and self.name):
            raise ValidationError(
                {
                    "member": _("Either specify a member or a name"),
                    "name": _("Either specify a member or a name"),
                }
            )

    @property
    def member_name(self):
        if self.member is not None:
            return self.member.get_full_name()
        return self.name

    @property
    def member_last_name(self):
        if self.member is not None:
            return self.member.last_name
        return " ".join(self.name.split(" ")[1:])

    @property
    def member_first_name(self):
        if self.member is not None:
            return self.member.first_name
        return self.name.strip(" ").split(" ")[0]

    @property
    def can_be_changed(self):
        try:
            return (
                not self.payment or self.payment.type == Payment.TPAY
            ) and not self.pizza_event.has_ended
        except ObjectDoesNotExist:
            return False

    class Meta:
        unique_together = (
            "pizza_event",
            "member",
        )

    def __str__(self):
        return _("Order by {member_name}: {product}").format(
            member_name=self.member_name, product=self.product
        )