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

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 _ 

6 

7import members 

8from events.models import Event 

9from payments.models import Payment, PaymentAmountField 

10from payments.services import delete_payment 

11 

12 

13class CurrentEventManager(models.Manager): 

14 """Only shows available products.""" 

15 

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 ) 

25 

26 

27class FoodEvent(models.Model): 

28 """Describes an event where food can be ordered.""" 

29 

30 objects = models.Manager() 

31 current_objects = CurrentEventManager() 

32 

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 ) 

38 

39 send_notification = models.BooleanField( 

40 _("Send an order notification"), default=True 

41 ) 

42 

43 tpay_allowed = models.BooleanField(_("Allow Thalia Pay"), default=True) 

44 

45 @property 

46 def title(self): 

47 return self.event.title 

48 

49 @property 

50 def in_the_future(self): 

51 return self.start > timezone.now() 

52 

53 @property 

54 def has_ended(self): 

55 return self.end < timezone.now() 

56 

57 @property 

58 def just_ended(self): 

59 return ( 

60 self.has_ended and self.end + timezone.timedelta(hours=8) > timezone.now() 

61 ) 

62 

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 

73 

74 def __init__(self, *args, **kwargs): 

75 super().__init__(*args, **kwargs) 

76 self._end = self.end 

77 

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 ) 

92 

93 def clean(self): 

94 super().clean() 

95 

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 ) 

103 

104 def __str__(self): 

105 return "Food for " + str(self.event) 

106 

107 class Meta: 

108 ordering = ("-start",) 

109 

110 

111class AvailableProductManager(models.Manager): 

112 """Only shows available products.""" 

113 

114 def get_queryset(self): 

115 return super().get_queryset().filter(available=True) 

116 

117 

118class Product(models.Model): 

119 """Describes a product.""" 

120 

121 objects = models.Manager() 

122 available_products = AvailableProductManager() 

123 

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 ) 

135 

136 def __str__(self): 

137 return self.name 

138 

139 class Meta: 

140 ordering = ("name",) 

141 permissions = (("order_restricted_products", _("Order restricted products")),) 

142 

143 

144class FoodOrder(models.Model): 

145 """Describes an order of an item during a food event.""" 

146 

147 member = models.ForeignKey( 

148 members.models.Member, 

149 on_delete=models.CASCADE, 

150 blank=True, 

151 null=True, 

152 ) 

153 

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 ) 

161 

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 ) 

170 

171 product = models.ForeignKey( 

172 verbose_name=_("product"), 

173 to=Product, 

174 on_delete=models.PROTECT, 

175 ) 

176 

177 food_event = models.ForeignKey( 

178 verbose_name=_("event"), 

179 to=FoodEvent, 

180 on_delete=models.CASCADE, 

181 related_name="orders", 

182 ) 

183 

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 ) 

192 

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 

198 

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:]) 

204 

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] 

210 

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 

219 

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) 

224 

225 class Meta: 

226 unique_together = ( 

227 "food_event", 

228 "member", 

229 ) 

230 

231 def __str__(self): 

232 return _("Food order by {member_name}: {product}").format( 

233 member_name=self.member_name, product=self.product 

234 )