sandwitches 2.5.3__tar.gz → 2.5.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sandwitches-2.5.3 → sandwitches-2.5.5}/PKG-INFO +1 -1
- {sandwitches-2.5.3 → sandwitches-2.5.5}/pyproject.toml +1 -1
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/admin.py +8 -3
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/api.py +17 -1
- sandwitches-2.5.5/src/sandwitches/migrations/0018_remove_order_recipe_alter_order_total_price_and_more.py +58 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/models.py +39 -26
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/tasks.py +27 -6
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/order_list.html +1 -1
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/partials/order_rows.html +5 -1
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/order_detail.html +8 -6
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/profile.html +10 -1
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/urls.py +0 -3
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/views.py +91 -28
- {sandwitches-2.5.3 → sandwitches-2.5.5}/README.md +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/__init__.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/asgi.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/feeds.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/forms.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/locale/nl/LC_MESSAGES/django.po +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/management/__init__.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/management/commands/__init__.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/management/commands/reset_daily_orders.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0001_initial.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0003_setting.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0005_rating_comment.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0010_rename_is_approved_historicalrecipe_is_community_made_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0012_rename_is_community_made_historicalrecipe_is_approved_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0013_cartitem.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0014_ensure_groups_exist.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0016_user_theme.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0017_setting_gotify_token_setting_gotify_url.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/__init__.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/settings.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/storage.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/admin_base.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/confirm_delete.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/dashboard.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/partials/dashboard_charts.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/rating_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/recipe_approval_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/recipe_form.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/recipe_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/settings.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/tag_form.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/tag_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/task_detail.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/task_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/user_form.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/user_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/base.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/base_beer.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/cart.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/community.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/carousel_scripts.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/favorites_search_form.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/footer.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/ingredients_scripts.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/ingredients_section.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/instructions_section.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/navbar.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/rating_section.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/recipe_header.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/search_form.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/search_scripts.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/side_menu.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/user_menu.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/detail.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/favorites.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/index.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/login.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/partials/recipe_list.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/settings.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/setup.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/signup.html +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templatetags/__init__.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templatetags/custom_filters.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templatetags/markdown_extras.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/utils.py +0 -0
- {sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/wsgi.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
2
|
from django.contrib.auth import get_user_model
|
|
3
3
|
from django.contrib.auth.admin import UserAdmin
|
|
4
|
-
from .models import Recipe, Tag, Rating, Setting, Order
|
|
4
|
+
from .models import Recipe, Tag, Rating, Setting, Order, OrderItem
|
|
5
5
|
from django.utils.html import format_html
|
|
6
6
|
from import_export import resources
|
|
7
7
|
from import_export.admin import ImportExportModelAdmin
|
|
@@ -81,18 +81,23 @@ class RatingAdmin(ImportExportModelAdmin):
|
|
|
81
81
|
resource_classes = [RatingResource]
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
class OrderItemInline(admin.TabularInline):
|
|
85
|
+
model = OrderItem
|
|
86
|
+
extra = 0
|
|
87
|
+
|
|
88
|
+
|
|
84
89
|
@admin.register(Order)
|
|
85
90
|
class OrderAdmin(ImportExportModelAdmin):
|
|
86
91
|
resource_classes = [OrderResource]
|
|
92
|
+
inlines = [OrderItemInline]
|
|
87
93
|
list_display = (
|
|
88
94
|
"id",
|
|
89
95
|
"user",
|
|
90
|
-
"recipe",
|
|
91
96
|
"status",
|
|
92
97
|
"completed",
|
|
93
98
|
"total_price",
|
|
94
99
|
"created_at",
|
|
95
100
|
)
|
|
96
101
|
list_filter = ("status", "completed", "created_at")
|
|
97
|
-
search_fields = ("user__username",
|
|
102
|
+
search_fields = ("user__username",)
|
|
98
103
|
readonly_fields = ("total_price", "created_at", "updated_at")
|
|
@@ -220,9 +220,25 @@ def get_tag(request, tag_id: int):
|
|
|
220
220
|
|
|
221
221
|
@api.post("v1/orders", auth=django_auth, response={201: OrderSchema, 400: Error})
|
|
222
222
|
def create_order(request, payload: CreateOrderSchema):
|
|
223
|
+
from .models import OrderItem
|
|
224
|
+
from django.db import transaction
|
|
225
|
+
from .tasks import notify_order_submitted, send_gotify_notification
|
|
226
|
+
|
|
223
227
|
recipe = get_object_or_404(Recipe, id=payload.recipe_id)
|
|
224
228
|
try:
|
|
225
|
-
|
|
229
|
+
with transaction.atomic():
|
|
230
|
+
order = Order.objects.create(user=request.user) # ty:ignore[unresolved-attribute]
|
|
231
|
+
OrderItem.objects.create(order=order, recipe=recipe, quantity=1) # ty:ignore[unresolved-attribute]
|
|
232
|
+
order.total_price = recipe.price
|
|
233
|
+
order.save()
|
|
234
|
+
|
|
235
|
+
notify_order_submitted.enqueue(order_id=order.pk)
|
|
236
|
+
send_gotify_notification.enqueue(
|
|
237
|
+
title="New Order Received",
|
|
238
|
+
message=f"Order #{order.pk} by {request.user.username}. Total: {order.total_price}€",
|
|
239
|
+
priority=6,
|
|
240
|
+
)
|
|
241
|
+
|
|
226
242
|
return 201, order
|
|
227
243
|
except (ValidationError, ValueError) as e:
|
|
228
244
|
return 400, {"message": str(e)}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-02-02 19:20
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("sandwitches", "0017_setting_gotify_token_setting_gotify_url"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name="order",
|
|
15
|
+
name="recipe",
|
|
16
|
+
),
|
|
17
|
+
migrations.AlterField(
|
|
18
|
+
model_name="order",
|
|
19
|
+
name="total_price",
|
|
20
|
+
field=models.DecimalField(decimal_places=2, default=0, max_digits=6),
|
|
21
|
+
),
|
|
22
|
+
migrations.CreateModel(
|
|
23
|
+
name="OrderItem",
|
|
24
|
+
fields=[
|
|
25
|
+
(
|
|
26
|
+
"id",
|
|
27
|
+
models.BigAutoField(
|
|
28
|
+
auto_created=True,
|
|
29
|
+
primary_key=True,
|
|
30
|
+
serialize=False,
|
|
31
|
+
verbose_name="ID",
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
("quantity", models.PositiveIntegerField(default=1)),
|
|
35
|
+
("price", models.DecimalField(decimal_places=2, max_digits=6)),
|
|
36
|
+
(
|
|
37
|
+
"order",
|
|
38
|
+
models.ForeignKey(
|
|
39
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
40
|
+
related_name="items",
|
|
41
|
+
to="sandwitches.order",
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
"recipe",
|
|
46
|
+
models.ForeignKey(
|
|
47
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
48
|
+
related_name="order_items",
|
|
49
|
+
to="sandwitches.recipe",
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
options={
|
|
54
|
+
"verbose_name": "Order Item",
|
|
55
|
+
"verbose_name_plural": "Order Items",
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
]
|
|
@@ -276,10 +276,9 @@ class Order(models.Model):
|
|
|
276
276
|
user = models.ForeignKey(
|
|
277
277
|
settings.AUTH_USER_MODEL, related_name="orders", on_delete=models.CASCADE
|
|
278
278
|
)
|
|
279
|
-
recipe = models.ForeignKey(Recipe, related_name="orders", on_delete=models.CASCADE)
|
|
280
279
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
|
|
281
280
|
completed = models.BooleanField(default=False)
|
|
282
|
-
total_price = models.DecimalField(max_digits=6, decimal_places=2)
|
|
281
|
+
total_price = models.DecimalField(max_digits=6, decimal_places=2, default=0)
|
|
283
282
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
284
283
|
updated_at = models.DateTimeField(auto_now=True)
|
|
285
284
|
|
|
@@ -289,40 +288,54 @@ class Order(models.Model):
|
|
|
289
288
|
verbose_name_plural = "Orders"
|
|
290
289
|
|
|
291
290
|
def save(self, *args, **kwargs):
|
|
292
|
-
if not self.recipe.price: # ty:ignore[possibly-missing-attribute]
|
|
293
|
-
raise ValueError("Cannot order a recipe without a price.")
|
|
294
|
-
if not self.total_price:
|
|
295
|
-
self.total_price = self.recipe.price # ty:ignore[possibly-missing-attribute]
|
|
296
|
-
|
|
297
291
|
is_new = self.pk is None
|
|
298
|
-
if is_new:
|
|
299
|
-
# We use select_for_update to lock the row and prevent race conditions
|
|
300
|
-
# However, since 'self.recipe' is already fetched, we need to re-fetch it with lock if we want to be strict.
|
|
301
|
-
# For simplicity in this context, we will reload it or trust the current instance but ideally:
|
|
302
|
-
|
|
303
|
-
# We need to wrap this in a transaction if not already
|
|
304
|
-
# But simple increment logic:
|
|
305
|
-
if (
|
|
306
|
-
self.recipe.max_daily_orders is not None # ty:ignore[possibly-missing-attribute]
|
|
307
|
-
and self.recipe.daily_orders_count >= self.recipe.max_daily_orders # ty:ignore[possibly-missing-attribute]
|
|
308
|
-
):
|
|
309
|
-
raise ValidationError("Daily order limit reached for this recipe.")
|
|
310
|
-
|
|
311
|
-
self.recipe.daily_orders_count += 1 # ty:ignore[possibly-missing-attribute]
|
|
312
|
-
self.recipe.save(update_fields=["daily_orders_count"]) # ty:ignore[possibly-missing-attribute]
|
|
313
|
-
|
|
314
292
|
super().save(*args, **kwargs)
|
|
315
|
-
|
|
316
293
|
if is_new:
|
|
317
294
|
notify_order_submitted.enqueue(order_id=self.pk)
|
|
318
295
|
send_gotify_notification.enqueue(
|
|
319
296
|
title="New Order Received",
|
|
320
|
-
message=f"Order #{self.pk}
|
|
297
|
+
message=f"Order #{self.pk} by {self.user.username}. Total: {self.total_price}€",
|
|
321
298
|
priority=6,
|
|
322
299
|
)
|
|
323
300
|
|
|
324
301
|
def __str__(self):
|
|
325
|
-
return f"Order #{self.pk} - {self.user}
|
|
302
|
+
return f"Order #{self.pk} - {self.user}"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class OrderItem(models.Model):
|
|
306
|
+
order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE)
|
|
307
|
+
recipe = models.ForeignKey(
|
|
308
|
+
Recipe, related_name="order_items", on_delete=models.CASCADE
|
|
309
|
+
)
|
|
310
|
+
quantity = models.PositiveIntegerField(default=1)
|
|
311
|
+
price = models.DecimalField(max_digits=6, decimal_places=2)
|
|
312
|
+
|
|
313
|
+
class Meta:
|
|
314
|
+
verbose_name = "Order Item"
|
|
315
|
+
verbose_name_plural = "Order Items"
|
|
316
|
+
|
|
317
|
+
def save(self, *args, **kwargs):
|
|
318
|
+
if not self.price:
|
|
319
|
+
self.price = self.recipe.price
|
|
320
|
+
|
|
321
|
+
is_new = self.pk is None
|
|
322
|
+
if is_new:
|
|
323
|
+
if (
|
|
324
|
+
self.recipe.max_daily_orders is not None
|
|
325
|
+
and self.recipe.daily_orders_count + self.quantity
|
|
326
|
+
> self.recipe.max_daily_orders
|
|
327
|
+
):
|
|
328
|
+
raise ValidationError(
|
|
329
|
+
f"Daily order limit reached for {self.recipe.title}."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
self.recipe.daily_orders_count += self.quantity
|
|
333
|
+
self.recipe.save(update_fields=["daily_orders_count"])
|
|
334
|
+
|
|
335
|
+
super().save(*args, **kwargs)
|
|
336
|
+
|
|
337
|
+
def __str__(self):
|
|
338
|
+
return f"{self.quantity}x {self.recipe.title} in Order #{self.order.pk}"
|
|
326
339
|
|
|
327
340
|
|
|
328
341
|
class CartItem(models.Model):
|
|
@@ -49,7 +49,11 @@ def notify_order_submitted(order_id):
|
|
|
49
49
|
from .models import Order
|
|
50
50
|
|
|
51
51
|
try:
|
|
52
|
-
order =
|
|
52
|
+
order = (
|
|
53
|
+
Order.objects.select_related("user") # ty:ignore[unresolved-attribute]
|
|
54
|
+
.prefetch_related("items__recipe")
|
|
55
|
+
.get(pk=order_id)
|
|
56
|
+
) # ty:ignore[unresolved-attribute]
|
|
53
57
|
except Order.DoesNotExist: # ty:ignore[unresolved-attribute]
|
|
54
58
|
logging.warning(f"Order {order_id} not found. Skipping notification.")
|
|
55
59
|
return
|
|
@@ -59,22 +63,38 @@ def notify_order_submitted(order_id):
|
|
|
59
63
|
logging.warning(f"User {user.username} has no email. Skipping notification.")
|
|
60
64
|
return
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
items = order.items.all()
|
|
67
|
+
if not items:
|
|
68
|
+
logging.warning(f"Order {order_id} has no items. Skipping notification.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Construct item list string
|
|
72
|
+
item_lines = []
|
|
73
|
+
for item in items:
|
|
74
|
+
item_lines.append(f"- {item.quantity}x {item.recipe.title}")
|
|
75
|
+
|
|
76
|
+
items_summary = "\n".join(item_lines)
|
|
77
|
+
items_html_list = "".join(
|
|
78
|
+
[f"<li>{item.quantity}x {item.recipe.title}</li>" for item in items]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
subject = _("Order Confirmation: Order #%(order_id)s") % {"order_id": order.id}
|
|
64
82
|
from_email = getattr(settings, "EMAIL_FROM_ADDRESS")
|
|
65
83
|
|
|
66
84
|
context_data = {
|
|
67
85
|
"user_name": user.get_full_name() or user.username,
|
|
68
|
-
"
|
|
86
|
+
"items_summary": items_summary,
|
|
69
87
|
"order_id": order.id,
|
|
70
88
|
"total_price": order.total_price,
|
|
89
|
+
"items_html_list": items_html_list,
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
text_content = (
|
|
74
93
|
_(
|
|
75
94
|
"Hello %(user_name)s,\n\n"
|
|
76
|
-
"Your order
|
|
95
|
+
"Your order has been successfully submitted!\n"
|
|
77
96
|
"Order ID: %(order_id)s\n"
|
|
97
|
+
"Items:\n%(items_summary)s\n"
|
|
78
98
|
"Total Price: %(total_price)s\n\n"
|
|
79
99
|
"Thank you for ordering with Sandwitches.\n"
|
|
80
100
|
)
|
|
@@ -86,9 +106,10 @@ def notify_order_submitted(order_id):
|
|
|
86
106
|
"<div style='font-family: sans-serif;'>"
|
|
87
107
|
"<h2>Order Confirmation</h2>"
|
|
88
108
|
"<p>Hello <strong>%(user_name)s</strong>,</p>"
|
|
89
|
-
"<p>Your order
|
|
109
|
+
"<p>Your order has been successfully submitted!</p>"
|
|
90
110
|
"<ul>"
|
|
91
111
|
"<li>Order ID: %(order_id)s</li>"
|
|
112
|
+
"%(items_html_list)s"
|
|
92
113
|
"<li>Total Price: %(total_price)s</li>"
|
|
93
114
|
"</ul>"
|
|
94
115
|
"<p>Thank you for ordering with Sandwitches.</p>"
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/partials/order_rows.html
RENAMED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
<span class="max">{{ order.user.username }}</span>
|
|
13
13
|
</div>
|
|
14
14
|
</td>
|
|
15
|
-
<td>
|
|
15
|
+
<td>
|
|
16
|
+
{% for item in order.items.all %}
|
|
17
|
+
<div>{{ item.quantity }}x {{ item.recipe.title }}</div>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
</td>
|
|
16
20
|
<td>€ {{ order.total_price }} </td>
|
|
17
21
|
<td>
|
|
18
22
|
<form method="post" action="{% url 'admin_order_update_status' order.pk %}" class="row align-center no-space">
|
|
@@ -35,22 +35,24 @@
|
|
|
35
35
|
<div class="divider"></div>
|
|
36
36
|
|
|
37
37
|
<h6>{% trans "Item Details" %}</h6>
|
|
38
|
+
{% for item in order.items.all %}
|
|
38
39
|
<div class="row">
|
|
39
40
|
<div class="max">
|
|
40
|
-
<a href="{% url 'recipe_detail'
|
|
41
|
-
<p class="small-text">{{
|
|
41
|
+
<a href="{% url 'recipe_detail' item.recipe.slug %}" class="bold primary-text">{{ item.quantity }}x {{ item.recipe.title }}</a>
|
|
42
|
+
<p class="small-text">{{ item.recipe.description|truncatewords:20 }}</p>
|
|
42
43
|
</div>
|
|
43
44
|
<div class="min">
|
|
44
|
-
|
|
45
|
+
<!-- Calculate item total: price * quantity -->
|
|
46
|
+
<span class="bold">{% widthratio item.price 1 item.quantity %} €</span>
|
|
45
47
|
</div>
|
|
46
48
|
</div>
|
|
47
49
|
|
|
48
|
-
{% if
|
|
50
|
+
{% if item.recipe.image %}
|
|
49
51
|
<div class="space"></div>
|
|
50
|
-
<img src="{{
|
|
52
|
+
<img src="{{ item.recipe.image.url }}" class="responsive round border" alt="{{ item.recipe.title }}" style="max-height: 200px; object-fit: cover;">
|
|
51
53
|
{% endif %}
|
|
52
|
-
|
|
53
54
|
<div class="divider"></div>
|
|
55
|
+
{% endfor %}
|
|
54
56
|
|
|
55
57
|
<div class="row">
|
|
56
58
|
<div class="max text-right">
|
|
@@ -136,7 +136,16 @@
|
|
|
136
136
|
<tr>
|
|
137
137
|
<td>{{ order.id }}</td>
|
|
138
138
|
<td>
|
|
139
|
-
|
|
139
|
+
{% with first_item=order.items.first %}
|
|
140
|
+
{% if first_item %}
|
|
141
|
+
<a href="{% url 'recipe_detail' first_item.recipe.slug %}">{{ first_item.recipe.title }}</a>
|
|
142
|
+
{% if order.items.count > 1 %}
|
|
143
|
+
<span class="small-text">(+{{ order.items.count|add:"-1" }})</span>
|
|
144
|
+
{% endif %}
|
|
145
|
+
{% else %}
|
|
146
|
+
<span>{% trans "No items" %}</span>
|
|
147
|
+
{% endif %}
|
|
148
|
+
{% endwith %}
|
|
140
149
|
</td>
|
|
141
150
|
<td>{{ order.created_at|date:"d/m/Y" }}</td>
|
|
142
151
|
<td>
|
|
@@ -50,9 +50,6 @@ urlpatterns = [
|
|
|
50
50
|
path("cart/checkout/", views.checkout_cart, name="checkout_cart"),
|
|
51
51
|
path("", views.index, name="index"),
|
|
52
52
|
path("feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"),
|
|
53
|
-
path(
|
|
54
|
-
"feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"
|
|
55
|
-
), # Add this line
|
|
56
53
|
]
|
|
57
54
|
|
|
58
55
|
urlpatterns += i18n_patterns(
|
|
@@ -9,7 +9,11 @@ from django.contrib.auth.decorators import login_required
|
|
|
9
9
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
10
10
|
from django.utils.translation import gettext as _
|
|
11
11
|
from django.utils import translation
|
|
12
|
-
from .models import Recipe, Rating, Tag, Order, CartItem, Setting
|
|
12
|
+
from .models import Recipe, Rating, Tag, Order, CartItem, Setting, OrderItem
|
|
13
|
+
from .tasks import (
|
|
14
|
+
notify_order_submitted,
|
|
15
|
+
send_gotify_notification,
|
|
16
|
+
)
|
|
13
17
|
from .utils import ORDER_DB
|
|
14
18
|
from .forms import (
|
|
15
19
|
RecipeForm,
|
|
@@ -542,7 +546,8 @@ def admin_order_update_status(request, pk):
|
|
|
542
546
|
@staff_member_required
|
|
543
547
|
def admin_order_list(request):
|
|
544
548
|
orders = (
|
|
545
|
-
Order.objects.select_related("user"
|
|
549
|
+
Order.objects.select_related("user") # ty:ignore[unresolved-attribute]
|
|
550
|
+
.prefetch_related("items__recipe")
|
|
546
551
|
.all()
|
|
547
552
|
.order_by("-created_at")
|
|
548
553
|
)
|
|
@@ -626,12 +631,27 @@ def order_recipe(request, pk):
|
|
|
626
631
|
"""
|
|
627
632
|
Create an order for the given recipe by the logged-in user.
|
|
628
633
|
"""
|
|
634
|
+
from django.db import transaction
|
|
635
|
+
from .models import OrderItem
|
|
636
|
+
|
|
629
637
|
recipe = get_object_or_404(Recipe, pk=pk)
|
|
630
638
|
if request.method != "POST":
|
|
631
639
|
return redirect("recipe_detail", slug=recipe.slug)
|
|
632
640
|
|
|
633
641
|
try:
|
|
634
|
-
|
|
642
|
+
with transaction.atomic():
|
|
643
|
+
order = Order.objects.create(user=request.user) # ty:ignore[unresolved-attribute]
|
|
644
|
+
OrderItem.objects.create(order=order, recipe=recipe, quantity=1) # ty:ignore[unresolved-attribute]
|
|
645
|
+
order.total_price = recipe.price
|
|
646
|
+
order.save()
|
|
647
|
+
|
|
648
|
+
notify_order_submitted.enqueue(order_id=order.pk)
|
|
649
|
+
send_gotify_notification.enqueue(
|
|
650
|
+
title="New Order Received",
|
|
651
|
+
message=f"Order #{order.pk} by {request.user.username}. Total: {order.total_price}€",
|
|
652
|
+
priority=6,
|
|
653
|
+
)
|
|
654
|
+
|
|
635
655
|
logging.debug(f"Created {order}")
|
|
636
656
|
messages.success(
|
|
637
657
|
request,
|
|
@@ -918,7 +938,7 @@ def user_profile(request):
|
|
|
918
938
|
else:
|
|
919
939
|
form = UserProfileForm(instance=request.user)
|
|
920
940
|
|
|
921
|
-
orders = request.user.orders.
|
|
941
|
+
orders = request.user.orders.prefetch_related("items__recipe").all()
|
|
922
942
|
|
|
923
943
|
# Filtering
|
|
924
944
|
status_filter = request.GET.get("status")
|
|
@@ -1052,39 +1072,82 @@ def update_cart_quantity(request, pk):
|
|
|
1052
1072
|
|
|
1053
1073
|
@login_required
|
|
1054
1074
|
def checkout_cart(request):
|
|
1055
|
-
|
|
1075
|
+
if request.method != "POST":
|
|
1076
|
+
return redirect("view_cart")
|
|
1077
|
+
|
|
1078
|
+
cart_items = CartItem.objects.filter(user=request.user).select_related("recipe") # ty:ignore[unresolved-attribute]
|
|
1056
1079
|
if not cart_items.exists():
|
|
1057
1080
|
messages.error(request, _("Your cart is empty."))
|
|
1058
1081
|
return redirect("view_cart")
|
|
1059
1082
|
|
|
1060
|
-
created_orders = [] # noqa: F841
|
|
1061
|
-
errors = []
|
|
1062
|
-
|
|
1063
|
-
# We use a transaction to ensure either all orders are created or none if something goes wrong
|
|
1064
1083
|
from django.db import transaction
|
|
1065
1084
|
|
|
1066
1085
|
try:
|
|
1067
1086
|
with transaction.atomic():
|
|
1087
|
+
# 1. Validate all items have prices and satisfy limits
|
|
1088
|
+
for item in cart_items:
|
|
1089
|
+
if not item.recipe.price:
|
|
1090
|
+
raise ValidationError(
|
|
1091
|
+
_(
|
|
1092
|
+
"Recipe '%(title)s' cannot be ordered because it has no price."
|
|
1093
|
+
)
|
|
1094
|
+
% {"title": item.recipe.title}
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
if (
|
|
1098
|
+
item.recipe.max_daily_orders is not None
|
|
1099
|
+
and item.recipe.daily_orders_count + item.quantity
|
|
1100
|
+
> item.recipe.max_daily_orders
|
|
1101
|
+
):
|
|
1102
|
+
raise ValidationError(
|
|
1103
|
+
_("Daily order limit reached for %(title)s.")
|
|
1104
|
+
% {"title": item.recipe.title}
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
# 2. Create the order
|
|
1108
|
+
order = Order.objects.create(user=request.user) # ty:ignore[unresolved-attribute]
|
|
1109
|
+
total_price = 0
|
|
1110
|
+
|
|
1111
|
+
# 3. Create order items and update recipe counts
|
|
1068
1112
|
for item in cart_items:
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
Order.objects.create(user=request.user, recipe=item.recipe) # ty:ignore[unresolved-attribute]
|
|
1077
|
-
except (ValidationError, ValueError) as e:
|
|
1078
|
-
errors.append(f"{item.recipe.title}: {str(e)}")
|
|
1079
|
-
raise e # Trigger rollback
|
|
1113
|
+
order_item = OrderItem.objects.create( # ty:ignore[unresolved-attribute]
|
|
1114
|
+
order=order,
|
|
1115
|
+
recipe=item.recipe,
|
|
1116
|
+
quantity=item.quantity,
|
|
1117
|
+
price=item.recipe.price,
|
|
1118
|
+
)
|
|
1119
|
+
total_price += order_item.price * order_item.quantity
|
|
1080
1120
|
|
|
1121
|
+
# Note: OrderItem.save already updates daily_orders_count
|
|
1122
|
+
# But since we already checked it, it should be fine.
|
|
1123
|
+
|
|
1124
|
+
# 4. Finalize order
|
|
1125
|
+
order.total_price = total_price
|
|
1126
|
+
order.save()
|
|
1127
|
+
|
|
1128
|
+
# 5. Clear cart
|
|
1081
1129
|
cart_items.delete()
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1130
|
+
|
|
1131
|
+
# 6. Notifications (outside atomic block for better reliability if using DB-backed tasks)
|
|
1132
|
+
notify_order_submitted.enqueue(order_id=order.pk)
|
|
1133
|
+
send_gotify_notification.enqueue(
|
|
1134
|
+
title="New Order Received",
|
|
1135
|
+
message=f"Order #{order.pk} by {request.user.username}. Total: {order.total_price}€",
|
|
1136
|
+
priority=6,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
messages.success(request, _("Orders submitted successfully!"))
|
|
1140
|
+
return redirect("user_profile")
|
|
1141
|
+
|
|
1142
|
+
except ValidationError as e:
|
|
1143
|
+
# Specific validation error (e.g. price missing or limit reached)
|
|
1144
|
+
messages.error(request, str(e))
|
|
1145
|
+
return redirect("view_cart")
|
|
1146
|
+
except Exception as e:
|
|
1147
|
+
# General error
|
|
1148
|
+
logging.exception("Error during checkout")
|
|
1149
|
+
messages.error(
|
|
1150
|
+
request,
|
|
1151
|
+
_("An error occurred during checkout: %(error)s") % {"error": str(e)},
|
|
1152
|
+
)
|
|
1090
1153
|
return redirect("view_cart")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/management/commands/reset_daily_orders.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/migrations/0014_ensure_groups_exist.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/admin/recipe_approval_list.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/carousel_scripts.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/rating_section.html
RENAMED
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/recipe_header.html
RENAMED
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/search_form.html
RENAMED
|
File without changes
|
{sandwitches-2.5.3 → sandwitches-2.5.5}/src/sandwitches/templates/components/search_scripts.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|