Coverage for website/pizzas/models.py: 79.23%
112 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1from django.core.exceptions import ObjectDoesNotExist, ValidationError
2from django.db import models
3from django.db.models import Q
4from django.utils import timezone
5from django.utils.translation import gettext_lazy as _
7import members
8from events.models import Event
9from payments.models import Payment, PaymentAmountField
10from payments.services import delete_payment
13class CurrentEventManager(models.Manager):
14 """Only shows available products."""
16 def get_queryset(self):
17 return (
18 super()
19 .get_queryset()
20 .filter(
21 end__gt=timezone.now() - timezone.timedelta(hours=8),
22 start__lte=timezone.now() + timezone.timedelta(hours=8),
23 )
24 )
27class FoodEvent(models.Model):
28 """Describes an event where food can be ordered."""
30 objects = models.Manager()
31 current_objects = CurrentEventManager()
33 start = models.DateTimeField(_("Order from"))
34 end = models.DateTimeField(_("Order until"))
35 event = models.OneToOneField(
36 Event, on_delete=models.CASCADE, related_name="food_event"
37 )
39 send_notification = models.BooleanField(
40 _("Send an order notification"), default=True
41 )
43 tpay_allowed = models.BooleanField(_("Allow Thalia Pay"), default=True)
45 @property
46 def title(self):
47 return self.event.title
49 @property
50 def in_the_future(self):
51 return self.start > timezone.now()
53 @property
54 def has_ended(self):
55 return self.end < timezone.now()
57 @property
58 def just_ended(self):
59 return (
60 self.has_ended and self.end + timezone.timedelta(hours=8) > timezone.now()
61 )
63 @classmethod
64 def current(cls):
65 """Get the currently relevant pizza event: the first one that starts within 8 hours from now."""
66 try:
67 events = FoodEvent.current_objects.order_by("start")
68 if events.count() > 1:
69 return events.exclude(end__lt=timezone.now()).first()
70 return events.get()
71 except FoodEvent.DoesNotExist:
72 return None
74 def __init__(self, *args, **kwargs):
75 super().__init__(*args, **kwargs)
76 self._end = self.end
78 def validate_unique(self, exclude=None):
79 super().validate_unique(exclude)
80 for other in FoodEvent.objects.filter(
81 Q(end__gte=self.start, end__lte=self.end)
82 | Q(start=self.start, start__lte=self.start)
83 ):
84 if other.pk == self.pk:
85 continue
86 raise ValidationError(
87 {
88 "start": _("This event cannot overlap with {}.").format(other),
89 "end": _("This event cannot overlap with {}.").format(other),
90 }
91 )
93 def clean(self):
94 super().clean()
96 if self.start >= self.end: 96 ↛ exitline 96 didn't return from function 'clean' because the condition on line 96 was always true
97 raise ValidationError(
98 {
99 "start": _("The start is after the end of this event."),
100 "end": _("The end is before the start of this event."),
101 }
102 )
104 def __str__(self):
105 return "Food for " + str(self.event)
107 class Meta:
108 ordering = ("-start",)
111class AvailableProductManager(models.Manager):
112 """Only shows available products."""
114 def get_queryset(self):
115 return super().get_queryset().filter(available=True)
118class Product(models.Model):
119 """Describes a product."""
121 objects = models.Manager()
122 available_products = AvailableProductManager()
124 name = models.CharField(max_length=50)
125 description = models.TextField()
126 price = PaymentAmountField()
127 available = models.BooleanField(default=True)
128 restricted = models.BooleanField(
129 default=False,
130 help_text=_(
131 "Only allow to be ordered by people with the "
132 "'order restricted products' permission."
133 ),
134 )
136 def __str__(self):
137 return self.name
139 class Meta:
140 ordering = ("name",)
141 permissions = (("order_restricted_products", _("Order restricted products")),)
144class FoodOrder(models.Model):
145 """Describes an order of an item during a food event."""
147 member = models.ForeignKey(
148 members.models.Member,
149 on_delete=models.CASCADE,
150 blank=True,
151 null=True,
152 )
154 name = models.CharField(
155 verbose_name=_("name"),
156 max_length=50,
157 help_text=_("Use this for non-members"),
158 null=True,
159 blank=True,
160 )
162 payment = models.OneToOneField(
163 verbose_name=_("payment"),
164 to="payments.Payment",
165 related_name="food_order",
166 on_delete=models.SET_NULL,
167 blank=True,
168 null=True,
169 )
171 product = models.ForeignKey(
172 verbose_name=_("product"),
173 to=Product,
174 on_delete=models.PROTECT,
175 )
177 food_event = models.ForeignKey(
178 verbose_name=_("event"),
179 to=FoodEvent,
180 on_delete=models.CASCADE,
181 related_name="orders",
182 )
184 def clean(self):
185 if (self.member is None and not self.name) or (self.member and self.name):
186 raise ValidationError(
187 {
188 "member": _("Either specify a member or a name"),
189 "name": _("Either specify a member or a name"),
190 }
191 )
193 @property
194 def member_name(self):
195 if self.member is not None: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 return self.member.get_full_name()
197 return self.name
199 @property
200 def member_last_name(self):
201 if self.member is not None:
202 return self.member.last_name
203 return " ".join(self.name.split(" ")[1:])
205 @property
206 def member_first_name(self):
207 if self.member is not None:
208 return self.member.first_name
209 return self.name.strip(" ").split(" ", maxsplit=1)[0]
211 @property
212 def can_be_changed(self):
213 try:
214 return (
215 not self.payment or self.payment.type == Payment.TPAY
216 ) and not self.food_event.has_ended
217 except ObjectDoesNotExist:
218 return False
220 def delete(self, using=None, keep_parents=False):
221 if self.payment is not None and self.can_be_changed: 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true
222 delete_payment(self)
223 return super().delete(using, keep_parents)
225 class Meta:
226 unique_together = (
227 "food_event",
228 "member",
229 )
231 def __str__(self):
232 return _("Food order by {member_name}: {product}").format(
233 member_name=self.member_name, product=self.product
234 )