sandwitches 2.2.0__tar.gz → 2.3.1__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.2.0 → sandwitches-2.3.1}/PKG-INFO +1 -1
- {sandwitches-2.2.0 → sandwitches-2.3.1}/pyproject.toml +1 -1
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/admin.py +23 -2
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/api.py +22 -1
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/forms.py +49 -0
- sandwitches-2.3.1/src/sandwitches/management/commands/reset_daily_orders.py +14 -0
- sandwitches-2.3.1/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py +86 -0
- sandwitches-2.3.1/src/sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py +36 -0
- sandwitches-2.3.1/src/sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py +22 -0
- sandwitches-2.3.1/src/sandwitches/migrations/__init__.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/models.py +63 -1
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/settings.py +1 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/tasks.py +74 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/admin_base.html +4 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/dashboard.html +125 -61
- sandwitches-2.3.1/src/sandwitches/templates/admin/order_list.html +30 -0
- sandwitches-2.3.1/src/sandwitches/templates/admin/partials/dashboard_charts.html +90 -0
- sandwitches-2.3.1/src/sandwitches/templates/admin/partials/order_rows.html +28 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/rating_list.html +2 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/recipe_form.html +26 -0
- sandwitches-2.3.1/src/sandwitches/templates/admin/recipe_list.html +114 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/base.html +12 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/navbar.html +10 -5
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/recipe_header.html +17 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/side_menu.html +4 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/partials/recipe_list.html +7 -0
- sandwitches-2.3.1/src/sandwitches/templates/profile.html +95 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/recipe_form.html +15 -1
- sandwitches-2.3.1/src/sandwitches/templatetags/__init__.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/urls.py +9 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/views.py +178 -21
- sandwitches-2.2.0/src/sandwitches/templates/admin/recipe_list.html +0 -64
- {sandwitches-2.2.0 → sandwitches-2.3.1}/README.md +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/__init__.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/asgi.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/feeds.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/locale/nl/LC_MESSAGES/django.po +0 -0
- {sandwitches-2.2.0/src/sandwitches/migrations → sandwitches-2.3.1/src/sandwitches/management}/__init__.py +0 -0
- {sandwitches-2.2.0/src/sandwitches/templatetags → sandwitches-2.3.1/src/sandwitches/management/commands}/__init__.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0001_initial.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0003_setting.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0005_rating_comment.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/storage.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/confirm_delete.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/tag_form.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/tag_list.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/task_detail.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/task_list.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/user_form.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/user_list.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/base_beer.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/carousel_scripts.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/favorites_search_form.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/footer.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/ingredients_scripts.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/ingredients_section.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/instructions_section.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/language_dialog.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/rating_section.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/search_form.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/search_scripts.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/user_menu.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/detail.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/favorites.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/index.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/login.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/setup.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templates/signup.html +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templatetags/custom_filters.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/templatetags/markdown_extras.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/src/sandwitches/utils.py +0 -0
- {sandwitches-2.2.0 → sandwitches-2.3.1}/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
|
|
4
|
+
from .models import Recipe, Tag, Rating, Setting, Order
|
|
5
5
|
from django.utils.html import format_html
|
|
6
6
|
from import_export import resources
|
|
7
7
|
from import_export.admin import ImportExportModelAdmin
|
|
@@ -24,6 +24,11 @@ class RatingResource(resources.ModelResource):
|
|
|
24
24
|
model = Rating
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class OrderResource(resources.ModelResource):
|
|
28
|
+
class Meta:
|
|
29
|
+
model = Order
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
User = get_user_model()
|
|
28
33
|
|
|
29
34
|
|
|
@@ -42,7 +47,14 @@ class CustomUserAdmin(UserAdmin):
|
|
|
42
47
|
@admin.register(Recipe)
|
|
43
48
|
class RecipeAdmin(ImportExportModelAdmin):
|
|
44
49
|
resource_classes = [RecipeResource]
|
|
45
|
-
list_display = (
|
|
50
|
+
list_display = (
|
|
51
|
+
"title",
|
|
52
|
+
"uploaded_by",
|
|
53
|
+
"price",
|
|
54
|
+
"created_at",
|
|
55
|
+
"is_highlighted",
|
|
56
|
+
"show_url",
|
|
57
|
+
)
|
|
46
58
|
list_editable = ("is_highlighted",)
|
|
47
59
|
readonly_fields = ("created_at", "updated_at")
|
|
48
60
|
|
|
@@ -67,3 +79,12 @@ class TagAdmin(ImportExportModelAdmin):
|
|
|
67
79
|
@admin.register(Rating)
|
|
68
80
|
class RatingAdmin(ImportExportModelAdmin):
|
|
69
81
|
resource_classes = [RatingResource]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@admin.register(Order)
|
|
85
|
+
class OrderAdmin(ImportExportModelAdmin):
|
|
86
|
+
resource_classes = [OrderResource]
|
|
87
|
+
list_display = ("id", "user", "recipe", "status", "total_price", "created_at")
|
|
88
|
+
list_filter = ("status", "created_at")
|
|
89
|
+
search_fields = ("user__username", "recipe__title")
|
|
90
|
+
readonly_fields = ("total_price", "created_at", "updated_at")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from ninja import NinjaAPI
|
|
3
|
-
from .models import Recipe, Tag, Setting, Rating
|
|
3
|
+
from .models import Recipe, Tag, Setting, Rating, Order
|
|
4
4
|
from django.contrib.auth import get_user_model
|
|
5
5
|
from .utils import (
|
|
6
6
|
parse_ingredient_line,
|
|
@@ -14,6 +14,7 @@ from django.shortcuts import get_object_or_404
|
|
|
14
14
|
from datetime import date
|
|
15
15
|
import random
|
|
16
16
|
from typing import List, Optional # Import typing hints
|
|
17
|
+
from django.core.exceptions import ValidationError
|
|
17
18
|
|
|
18
19
|
from ninja.security import django_auth
|
|
19
20
|
|
|
@@ -82,6 +83,16 @@ class ScaledIngredient(Schema): # New Schema for scaled ingredients
|
|
|
82
83
|
name: Optional[str]
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
class OrderSchema(ModelSchema):
|
|
87
|
+
class Meta:
|
|
88
|
+
model = Order
|
|
89
|
+
fields = ["id", "status", "total_price", "created_at"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CreateOrderSchema(Schema):
|
|
93
|
+
recipe_id: int
|
|
94
|
+
|
|
95
|
+
|
|
85
96
|
@api.get("ping")
|
|
86
97
|
def ping(request):
|
|
87
98
|
return {"status": "ok", "message": "pong"}
|
|
@@ -205,3 +216,13 @@ def get_tags(request):
|
|
|
205
216
|
def get_tag(request, tag_id: int):
|
|
206
217
|
tag = get_object_or_404(Tag, id=tag_id)
|
|
207
218
|
return tag
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@api.post("v1/orders", auth=django_auth, response={201: OrderSchema, 400: Error})
|
|
222
|
+
def create_order(request, payload: CreateOrderSchema):
|
|
223
|
+
recipe = get_object_or_404(Recipe, id=payload.recipe_id)
|
|
224
|
+
try:
|
|
225
|
+
order = Order.objects.create(user=request.user, recipe=recipe) # ty:ignore[unresolved-attribute]
|
|
226
|
+
return 201, order
|
|
227
|
+
except (ValidationError, ValueError) as e:
|
|
228
|
+
return 400, {"message": str(e)}
|
|
@@ -85,6 +85,18 @@ class UserSignupForm(UserCreationForm, BaseUserFormMixin):
|
|
|
85
85
|
return user
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
class UserProfileForm(forms.ModelForm):
|
|
89
|
+
class Meta:
|
|
90
|
+
model = User
|
|
91
|
+
fields = (
|
|
92
|
+
"first_name",
|
|
93
|
+
"last_name",
|
|
94
|
+
"email",
|
|
95
|
+
"avatar",
|
|
96
|
+
"bio",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
88
100
|
class UserEditForm(forms.ModelForm):
|
|
89
101
|
class Meta:
|
|
90
102
|
model = User
|
|
@@ -124,6 +136,10 @@ class RecipeForm(forms.ModelForm):
|
|
|
124
136
|
"description",
|
|
125
137
|
"ingredients",
|
|
126
138
|
"instructions",
|
|
139
|
+
"price",
|
|
140
|
+
"is_highlighted",
|
|
141
|
+
"is_approved",
|
|
142
|
+
"max_daily_orders",
|
|
127
143
|
]
|
|
128
144
|
widgets = {
|
|
129
145
|
"image": forms.FileInput(),
|
|
@@ -162,6 +178,39 @@ class RecipeForm(forms.ModelForm):
|
|
|
162
178
|
return recipe
|
|
163
179
|
|
|
164
180
|
|
|
181
|
+
class UserRecipeSubmissionForm(forms.ModelForm):
|
|
182
|
+
tags_string = forms.CharField(
|
|
183
|
+
required=False,
|
|
184
|
+
label=_("Tags (comma separated)"),
|
|
185
|
+
widget=forms.TextInput(attrs={"placeholder": _("e.g. spicy, vegan, quick")}),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
class Meta:
|
|
189
|
+
model = Recipe
|
|
190
|
+
fields = [
|
|
191
|
+
"title",
|
|
192
|
+
"image",
|
|
193
|
+
"description",
|
|
194
|
+
"ingredients",
|
|
195
|
+
"instructions",
|
|
196
|
+
"price",
|
|
197
|
+
"servings",
|
|
198
|
+
]
|
|
199
|
+
widgets = {
|
|
200
|
+
"image": forms.FileInput(),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
def save(self, commit=True):
|
|
204
|
+
recipe = super().save(commit=commit)
|
|
205
|
+
if commit:
|
|
206
|
+
recipe.set_tags_from_string(self.cleaned_data.get("tags_string", ""))
|
|
207
|
+
else:
|
|
208
|
+
self.save_m2m = lambda: recipe.set_tags_from_string(
|
|
209
|
+
self.cleaned_data.get("tags_string", "")
|
|
210
|
+
)
|
|
211
|
+
return recipe
|
|
212
|
+
|
|
213
|
+
|
|
165
214
|
class RatingForm(forms.Form):
|
|
166
215
|
"""Form for rating recipes (0-10) with an optional comment."""
|
|
167
216
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
from sandwitches.tasks import reset_daily_orders
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Command(BaseCommand):
|
|
6
|
+
help = "Resets the daily order count for all recipes. Should be run at midnight."
|
|
7
|
+
|
|
8
|
+
def handle(self, *args, **options):
|
|
9
|
+
task_result = reset_daily_orders.enqueue()
|
|
10
|
+
self.stdout.write(
|
|
11
|
+
self.style.SUCCESS(
|
|
12
|
+
f"Enqueued daily order count reset task (Result ID: {task_result.id})."
|
|
13
|
+
)
|
|
14
|
+
)
|
sandwitches-2.3.1/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-01-20 10:49
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("sandwitches", "0006_historicalrecipe_is_highlighted_and_more"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="historicalrecipe",
|
|
16
|
+
name="price",
|
|
17
|
+
field=models.DecimalField(
|
|
18
|
+
blank=True,
|
|
19
|
+
decimal_places=2,
|
|
20
|
+
max_digits=6,
|
|
21
|
+
null=True,
|
|
22
|
+
verbose_name="Price (€)",
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
migrations.AddField(
|
|
26
|
+
model_name="recipe",
|
|
27
|
+
name="price",
|
|
28
|
+
field=models.DecimalField(
|
|
29
|
+
blank=True,
|
|
30
|
+
decimal_places=2,
|
|
31
|
+
max_digits=6,
|
|
32
|
+
null=True,
|
|
33
|
+
verbose_name="Price (€)",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
migrations.CreateModel(
|
|
37
|
+
name="Order",
|
|
38
|
+
fields=[
|
|
39
|
+
(
|
|
40
|
+
"id",
|
|
41
|
+
models.BigAutoField(
|
|
42
|
+
auto_created=True,
|
|
43
|
+
primary_key=True,
|
|
44
|
+
serialize=False,
|
|
45
|
+
verbose_name="ID",
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
"status",
|
|
50
|
+
models.CharField(
|
|
51
|
+
choices=[
|
|
52
|
+
("PENDING", "Pending"),
|
|
53
|
+
("COMPLETED", "Completed"),
|
|
54
|
+
("CANCELLED", "Cancelled"),
|
|
55
|
+
],
|
|
56
|
+
default="PENDING",
|
|
57
|
+
max_length=20,
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
("total_price", models.DecimalField(decimal_places=2, max_digits=6)),
|
|
61
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
62
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
63
|
+
(
|
|
64
|
+
"recipe",
|
|
65
|
+
models.ForeignKey(
|
|
66
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
67
|
+
related_name="orders",
|
|
68
|
+
to="sandwitches.recipe",
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"user",
|
|
73
|
+
models.ForeignKey(
|
|
74
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
75
|
+
related_name="orders",
|
|
76
|
+
to=settings.AUTH_USER_MODEL,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
options={
|
|
81
|
+
"verbose_name": "Order",
|
|
82
|
+
"verbose_name_plural": "Orders",
|
|
83
|
+
"ordering": ("-created_at",),
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
]
|
sandwitches-2.3.1/src/sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-01-21 09:53
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0007_historicalrecipe_price_recipe_price_order"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="historicalrecipe",
|
|
14
|
+
name="daily_orders_count",
|
|
15
|
+
field=models.PositiveIntegerField(default=0),
|
|
16
|
+
),
|
|
17
|
+
migrations.AddField(
|
|
18
|
+
model_name="historicalrecipe",
|
|
19
|
+
name="max_daily_orders",
|
|
20
|
+
field=models.PositiveIntegerField(
|
|
21
|
+
blank=True, null=True, verbose_name="Max daily orders"
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
migrations.AddField(
|
|
25
|
+
model_name="recipe",
|
|
26
|
+
name="daily_orders_count",
|
|
27
|
+
field=models.PositiveIntegerField(default=0),
|
|
28
|
+
),
|
|
29
|
+
migrations.AddField(
|
|
30
|
+
model_name="recipe",
|
|
31
|
+
name="max_daily_orders",
|
|
32
|
+
field=models.PositiveIntegerField(
|
|
33
|
+
blank=True, null=True, verbose_name="Max daily orders"
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
]
|
sandwitches-2.3.1/src/sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-01-22 12:41
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0008_historicalrecipe_daily_orders_count_and_more"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="historicalrecipe",
|
|
14
|
+
name="is_approved",
|
|
15
|
+
field=models.BooleanField(default=True),
|
|
16
|
+
),
|
|
17
|
+
migrations.AddField(
|
|
18
|
+
model_name="recipe",
|
|
19
|
+
name="is_approved",
|
|
20
|
+
field=models.BooleanField(default=True),
|
|
21
|
+
),
|
|
22
|
+
]
|
|
File without changes
|
|
@@ -4,12 +4,13 @@ from .storage import HashedFilenameStorage
|
|
|
4
4
|
from simple_history.models import HistoricalRecords
|
|
5
5
|
from django.contrib.auth.models import AbstractUser
|
|
6
6
|
from django.db.models import Avg
|
|
7
|
-
from .tasks import email_users
|
|
7
|
+
from .tasks import email_users, notify_order_submitted
|
|
8
8
|
from django.conf import settings
|
|
9
9
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
10
10
|
import logging
|
|
11
11
|
from django.urls import reverse
|
|
12
12
|
from solo.models import SingletonModel
|
|
13
|
+
from django.core.exceptions import ValidationError
|
|
13
14
|
|
|
14
15
|
from imagekit.models import ImageSpecField
|
|
15
16
|
from imagekit.processors import ResizeToFill
|
|
@@ -89,6 +90,9 @@ class Recipe(models.Model):
|
|
|
89
90
|
ingredients = models.TextField(blank=True)
|
|
90
91
|
instructions = models.TextField(blank=True)
|
|
91
92
|
servings = models.IntegerField(default=1, validators=[MinValueValidator(1)])
|
|
93
|
+
price = models.DecimalField(
|
|
94
|
+
max_digits=6, decimal_places=2, null=True, blank=True, verbose_name="Price (€)"
|
|
95
|
+
)
|
|
92
96
|
uploaded_by = models.ForeignKey(
|
|
93
97
|
settings.AUTH_USER_MODEL,
|
|
94
98
|
related_name="recipes",
|
|
@@ -128,6 +132,11 @@ class Recipe(models.Model):
|
|
|
128
132
|
)
|
|
129
133
|
tags = models.ManyToManyField(Tag, blank=True, related_name="recipes")
|
|
130
134
|
is_highlighted = models.BooleanField(default=False)
|
|
135
|
+
is_approved = models.BooleanField(default=True)
|
|
136
|
+
max_daily_orders = models.PositiveIntegerField(
|
|
137
|
+
null=True, blank=True, verbose_name="Max daily orders"
|
|
138
|
+
)
|
|
139
|
+
daily_orders_count = models.PositiveIntegerField(default=0)
|
|
131
140
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
132
141
|
updated_at = models.DateTimeField(auto_now=True)
|
|
133
142
|
history = HistoricalRecords()
|
|
@@ -216,3 +225,56 @@ class Rating(models.Model):
|
|
|
216
225
|
|
|
217
226
|
def __str__(self):
|
|
218
227
|
return f"{self.recipe} — {self.score} by {self.user}"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class Order(models.Model):
|
|
231
|
+
STATUS_CHOICES = (
|
|
232
|
+
("PENDING", "Pending"),
|
|
233
|
+
("COMPLETED", "Completed"),
|
|
234
|
+
("CANCELLED", "Cancelled"),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
user = models.ForeignKey(
|
|
238
|
+
settings.AUTH_USER_MODEL, related_name="orders", on_delete=models.CASCADE
|
|
239
|
+
)
|
|
240
|
+
recipe = models.ForeignKey(Recipe, related_name="orders", on_delete=models.CASCADE)
|
|
241
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
|
|
242
|
+
total_price = models.DecimalField(max_digits=6, decimal_places=2)
|
|
243
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
244
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
245
|
+
|
|
246
|
+
class Meta:
|
|
247
|
+
ordering = ("-created_at",)
|
|
248
|
+
verbose_name = "Order"
|
|
249
|
+
verbose_name_plural = "Orders"
|
|
250
|
+
|
|
251
|
+
def save(self, *args, **kwargs):
|
|
252
|
+
if not self.recipe.price: # ty:ignore[possibly-missing-attribute]
|
|
253
|
+
raise ValueError("Cannot order a recipe without a price.")
|
|
254
|
+
if not self.total_price:
|
|
255
|
+
self.total_price = self.recipe.price # ty:ignore[possibly-missing-attribute]
|
|
256
|
+
|
|
257
|
+
is_new = self.pk is None
|
|
258
|
+
if is_new:
|
|
259
|
+
# We use select_for_update to lock the row and prevent race conditions
|
|
260
|
+
# However, since 'self.recipe' is already fetched, we need to re-fetch it with lock if we want to be strict.
|
|
261
|
+
# For simplicity in this context, we will reload it or trust the current instance but ideally:
|
|
262
|
+
|
|
263
|
+
# We need to wrap this in a transaction if not already
|
|
264
|
+
# But simple increment logic:
|
|
265
|
+
if (
|
|
266
|
+
self.recipe.max_daily_orders is not None # ty:ignore[possibly-missing-attribute]
|
|
267
|
+
and self.recipe.daily_orders_count >= self.recipe.max_daily_orders # ty:ignore[possibly-missing-attribute]
|
|
268
|
+
):
|
|
269
|
+
raise ValidationError("Daily order limit reached for this recipe.")
|
|
270
|
+
|
|
271
|
+
self.recipe.daily_orders_count += 1 # ty:ignore[possibly-missing-attribute]
|
|
272
|
+
self.recipe.save(update_fields=["daily_orders_count"]) # ty:ignore[possibly-missing-attribute]
|
|
273
|
+
|
|
274
|
+
super().save(*args, **kwargs)
|
|
275
|
+
|
|
276
|
+
if is_new:
|
|
277
|
+
notify_order_submitted.enqueue(order_id=self.pk)
|
|
278
|
+
|
|
279
|
+
def __str__(self):
|
|
280
|
+
return f"Order #{self.pk} - {self.user} - {self.recipe}"
|
|
@@ -35,6 +35,80 @@ def email_users(context, recipe_id):
|
|
|
35
35
|
return True
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
@task(priority=5)
|
|
39
|
+
def reset_daily_orders():
|
|
40
|
+
from .models import Recipe
|
|
41
|
+
|
|
42
|
+
count = Recipe.objects.update(daily_orders_count=0) # ty:ignore[unresolved-attribute]
|
|
43
|
+
logging.info(f"Successfully reset daily order count for {count} recipes.")
|
|
44
|
+
return count
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@task(priority=2, queue_name="emails")
|
|
48
|
+
def notify_order_submitted(order_id):
|
|
49
|
+
from .models import Order
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
order = Order.objects.select_related("user", "recipe").get(pk=order_id) # ty:ignore[unresolved-attribute]
|
|
53
|
+
except Order.DoesNotExist: # ty:ignore[unresolved-attribute]
|
|
54
|
+
logging.warning(f"Order {order_id} not found. Skipping notification.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
user = order.user
|
|
58
|
+
if not user.email:
|
|
59
|
+
logging.warning(f"User {user.username} has no email. Skipping notification.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
recipe = order.recipe
|
|
63
|
+
subject = _("Order Confirmation: %(recipe_title)s") % {"recipe_title": recipe.title}
|
|
64
|
+
from_email = getattr(settings, "EMAIL_FROM_ADDRESS")
|
|
65
|
+
|
|
66
|
+
context_data = {
|
|
67
|
+
"user_name": user.get_full_name() or user.username,
|
|
68
|
+
"recipe_title": recipe.title,
|
|
69
|
+
"order_id": order.id,
|
|
70
|
+
"total_price": order.total_price,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
text_content = (
|
|
74
|
+
_(
|
|
75
|
+
"Hello %(user_name)s,\n\n"
|
|
76
|
+
"Your order for %(recipe_title)s has been successfully submitted!\n"
|
|
77
|
+
"Order ID: %(order_id)s\n"
|
|
78
|
+
"Total Price: %(total_price)s\n\n"
|
|
79
|
+
"Thank you for ordering with Sandwitches.\n"
|
|
80
|
+
)
|
|
81
|
+
% context_data
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
html_content = (
|
|
85
|
+
_(
|
|
86
|
+
"<div style='font-family: sans-serif;'>"
|
|
87
|
+
"<h2>Order Confirmation</h2>"
|
|
88
|
+
"<p>Hello <strong>%(user_name)s</strong>,</p>"
|
|
89
|
+
"<p>Your order for <strong>%(recipe_title)s</strong> has been successfully submitted!</p>"
|
|
90
|
+
"<ul>"
|
|
91
|
+
"<li>Order ID: %(order_id)s</li>"
|
|
92
|
+
"<li>Total Price: %(total_price)s</li>"
|
|
93
|
+
"</ul>"
|
|
94
|
+
"<p>Thank you for ordering with Sandwitches.</p>"
|
|
95
|
+
"</div>"
|
|
96
|
+
)
|
|
97
|
+
% context_data
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
msg = EmailMultiAlternatives(
|
|
101
|
+
subject=subject,
|
|
102
|
+
body=text_content,
|
|
103
|
+
from_email=from_email,
|
|
104
|
+
to=[user.email],
|
|
105
|
+
)
|
|
106
|
+
msg.attach_alternative(html_content, "text/html")
|
|
107
|
+
msg.send()
|
|
108
|
+
|
|
109
|
+
logging.info(f"Order confirmation email sent to {user.email} for order {order.id}")
|
|
110
|
+
|
|
111
|
+
|
|
38
112
|
def send_emails(recipe_id, emails):
|
|
39
113
|
from .models import Recipe
|
|
40
114
|
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
<i>star</i>
|
|
69
69
|
<span>{% trans "Ratings" %}</span>
|
|
70
70
|
</a>
|
|
71
|
+
<a href="{% url 'admin_order_list' %}" class="{% if request.resolver_match.url_name == 'admin_order_list' %}active{% endif %}">
|
|
72
|
+
<i>shopping_cart</i>
|
|
73
|
+
<span>{% trans "Orders" %}</span>
|
|
74
|
+
</a>
|
|
71
75
|
<a href="{% url 'admin_task_list' %}" class="{% if request.resolver_match.url_name == 'admin_task_list' %}active{% endif %}">
|
|
72
76
|
<i>assignment</i>
|
|
73
77
|
<span>{% trans "Tasks" %}</span>
|