sandwitches 2.3.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.3.0 → sandwitches-2.3.1}/PKG-INFO +1 -1
- {sandwitches-2.3.0 → sandwitches-2.3.1}/pyproject.toml +1 -1
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/api.py +22 -1
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/forms.py +36 -0
- sandwitches-2.3.1/src/sandwitches/management/commands/reset_daily_orders.py +14 -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.3.0 → sandwitches-2.3.1}/src/sandwitches/models.py +28 -1
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/settings.py +1 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/tasks.py +74 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/admin_base.html +4 -0
- {sandwitches-2.3.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.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/rating_list.html +2 -0
- {sandwitches-2.3.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.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/base.html +12 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/recipe_header.html +9 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/side_menu.html +4 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/profile.html +1 -1
- {sandwitches-2.3.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.3.0 → sandwitches-2.3.1}/src/sandwitches/urls.py +8 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/views.py +162 -21
- sandwitches-2.3.0/src/sandwitches/templates/admin/recipe_list.html +0 -64
- {sandwitches-2.3.0 → sandwitches-2.3.1}/README.md +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/__init__.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/admin.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/asgi.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/feeds.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/locale/nl/LC_MESSAGES/django.po +0 -0
- {sandwitches-2.3.0/src/sandwitches/migrations → sandwitches-2.3.1/src/sandwitches/management}/__init__.py +0 -0
- {sandwitches-2.3.0/src/sandwitches/templatetags → sandwitches-2.3.1/src/sandwitches/management/commands}/__init__.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0001_initial.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0003_setting.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0005_rating_comment.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/storage.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/confirm_delete.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/tag_form.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/tag_list.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/task_detail.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/task_list.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/user_form.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/admin/user_list.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/base_beer.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/carousel_scripts.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/favorites_search_form.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/footer.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/ingredients_scripts.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/ingredients_section.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/instructions_section.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/language_dialog.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/navbar.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/rating_section.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/search_form.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/search_scripts.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/components/user_menu.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/detail.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/favorites.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/index.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/login.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/partials/recipe_list.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/setup.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templates/signup.html +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templatetags/custom_filters.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/templatetags/markdown_extras.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/utils.py +0 -0
- {sandwitches-2.3.0 → sandwitches-2.3.1}/src/sandwitches/wsgi.py +0 -0
|
@@ -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)}
|
|
@@ -137,6 +137,9 @@ class RecipeForm(forms.ModelForm):
|
|
|
137
137
|
"ingredients",
|
|
138
138
|
"instructions",
|
|
139
139
|
"price",
|
|
140
|
+
"is_highlighted",
|
|
141
|
+
"is_approved",
|
|
142
|
+
"max_daily_orders",
|
|
140
143
|
]
|
|
141
144
|
widgets = {
|
|
142
145
|
"image": forms.FileInput(),
|
|
@@ -175,6 +178,39 @@ class RecipeForm(forms.ModelForm):
|
|
|
175
178
|
return recipe
|
|
176
179
|
|
|
177
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
|
+
|
|
178
214
|
class RatingForm(forms.Form):
|
|
179
215
|
"""Form for rating recipes (0-10) with an optional comment."""
|
|
180
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/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
|
|
@@ -131,6 +132,11 @@ class Recipe(models.Model):
|
|
|
131
132
|
)
|
|
132
133
|
tags = models.ManyToManyField(Tag, blank=True, related_name="recipes")
|
|
133
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)
|
|
134
140
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
135
141
|
updated_at = models.DateTimeField(auto_now=True)
|
|
136
142
|
history = HistoricalRecords()
|
|
@@ -247,7 +253,28 @@ class Order(models.Model):
|
|
|
247
253
|
raise ValueError("Cannot order a recipe without a price.")
|
|
248
254
|
if not self.total_price:
|
|
249
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
|
+
|
|
250
274
|
super().save(*args, **kwargs)
|
|
251
275
|
|
|
276
|
+
if is_new:
|
|
277
|
+
notify_order_submitted.enqueue(order_id=self.pk)
|
|
278
|
+
|
|
252
279
|
def __str__(self):
|
|
253
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>
|
|
@@ -127,37 +127,147 @@
|
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
<!-- Charts Section -->
|
|
131
131
|
|
|
132
|
-
<div class="s12 m6">
|
|
133
132
|
|
|
134
|
-
<article class="round border padding">
|
|
135
133
|
|
|
136
|
-
<
|
|
134
|
+
<div class="s12">
|
|
137
135
|
|
|
138
|
-
<canvas id="recipeChart" style="width:100%; max-height:300px;"></canvas>
|
|
139
136
|
|
|
140
|
-
</article>
|
|
141
137
|
|
|
142
|
-
|
|
138
|
+
{% include "admin/partials/dashboard_charts.html" %}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
{% if pending_recipes %}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
<div class="s12">
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
<h5 class="bold error-text">{% trans "Pending Approvals" %}</h5>
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
<table class="border striped no-space">
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
<thead>
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
<tr>
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
<th>{% trans "Title" %}</th>
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
<th>{% trans "Uploader" %}</th>
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
<th>{% trans "Created At" %}</th>
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
<th class="right-align">{% trans "Actions" %}</th>
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
</tr>
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
</thead>
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
<tbody>
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
{% for recipe in pending_recipes %}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
<tr class="pointer" onclick="location.href='{% url 'admin_recipe_edit' recipe.pk %}'">
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
<td>{{ recipe.title }}</td>
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
<td>{{ recipe.uploaded_by.username|default:"-" }}</td>
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
<td>{{ recipe.created_at|date:"SHORT_DATETIME_FORMAT" }}</td>
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
<td class="right-align">
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
<a href="{% url 'admin_recipe_approve' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();" title="{% trans 'Approve' %}"><i>check</i></a>
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
<a href="{% url 'admin_recipe_edit' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();"><i>edit</i></a>
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
</td>
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
</tr>
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
{% endfor %}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
</tbody>
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
</table>
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
{% endif %}
|
|
143
259
|
|
|
144
|
-
<div class="s12 m6">
|
|
145
260
|
|
|
146
|
-
<article class="round border padding">
|
|
147
261
|
|
|
148
|
-
<h6 class="bold mb-1">{% trans "Average Rating Over Time (Last 30 Days)" %}</h6>
|
|
149
262
|
|
|
150
|
-
<canvas id="ratingChart" style="width:100%; max-height:300px;"></canvas>
|
|
151
263
|
|
|
152
|
-
</article>
|
|
153
264
|
|
|
154
|
-
</div>
|
|
155
265
|
|
|
266
|
+
<div class="s12">
|
|
156
267
|
|
|
157
268
|
|
|
158
|
-
<div class="s12">
|
|
159
269
|
|
|
160
|
-
|
|
270
|
+
<h5 class="bold">{% trans "Recent Recipes" %}</h5>
|
|
161
271
|
|
|
162
272
|
<table class="border striped no-space">
|
|
163
273
|
|
|
@@ -213,50 +323,4 @@
|
|
|
213
323
|
|
|
214
324
|
{% block admin_scripts %}
|
|
215
325
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
216
|
-
<script>
|
|
217
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
218
|
-
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#5d4037';
|
|
219
|
-
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary').trim() || '#ff7a18';
|
|
220
|
-
|
|
221
|
-
// Recipe Chart
|
|
222
|
-
new Chart(document.getElementById('recipeChart'), {
|
|
223
|
-
type: 'line',
|
|
224
|
-
data: {
|
|
225
|
-
labels: {{ recipe_labels|safe }},
|
|
226
|
-
datasets: [{
|
|
227
|
-
label: '{% trans "Recipes Created" %}',
|
|
228
|
-
data: {{ recipe_counts|safe }},
|
|
229
|
-
borderColor: primaryColor,
|
|
230
|
-
backgroundColor: primaryColor + '33',
|
|
231
|
-
fill: true,
|
|
232
|
-
tension: 0.4
|
|
233
|
-
}]
|
|
234
|
-
},
|
|
235
|
-
options: {
|
|
236
|
-
responsive: true,
|
|
237
|
-
plugins: { legend: { display: false } },
|
|
238
|
-
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Rating Chart
|
|
243
|
-
new Chart(document.getElementById('ratingChart'), {
|
|
244
|
-
type: 'bar',
|
|
245
|
-
data: {
|
|
246
|
-
labels: {{ rating_labels|safe }},
|
|
247
|
-
datasets: [{
|
|
248
|
-
label: '{% trans "Avg Rating" %}',
|
|
249
|
-
data: {{ rating_avgs|safe }},
|
|
250
|
-
backgroundColor: secondaryColor,
|
|
251
|
-
borderRadius: 4
|
|
252
|
-
}]
|
|
253
|
-
},
|
|
254
|
-
options: {
|
|
255
|
-
responsive: true,
|
|
256
|
-
plugins: { legend: { display: false } },
|
|
257
|
-
scales: { y: { min: 0, max: 10 } }
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
</script>
|
|
262
326
|
{% endblock %}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{% extends "admin/admin_base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block admin_title %}{% trans "Orders" %}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="padding">
|
|
8
|
+
<div class="row align-center">
|
|
9
|
+
<h5 class="max">{% trans "Orders" %}</h5>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="space"></div>
|
|
13
|
+
|
|
14
|
+
<table class="border">
|
|
15
|
+
<thead>
|
|
16
|
+
<tr>
|
|
17
|
+
<th>ID</th>
|
|
18
|
+
<th>{% trans "User" %}</th>
|
|
19
|
+
<th>{% trans "Recipe" %}</th>
|
|
20
|
+
<th>{% trans "Price" %}</th>
|
|
21
|
+
<th>{% trans "Status" %}</th>
|
|
22
|
+
<th>{% trans "Date" %}</th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody hx-get="{% url 'admin_order_list' %}" hx-trigger="every 5s">
|
|
26
|
+
{% include "admin/partials/order_rows.html" %}
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
</div>
|
|
30
|
+
{% endblock %}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
<div class="grid" hx-get="." hx-trigger="every 30s" hx-swap="outerHTML">
|
|
3
|
+
<div class="s12 m4">
|
|
4
|
+
<article class="round border padding">
|
|
5
|
+
<h6 class="bold mb-1">{% trans "Recipes Over Time" %}</h6>
|
|
6
|
+
<canvas id="recipeChart" style="width:100%; max-height:300px;"></canvas>
|
|
7
|
+
</article>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="s12 m4">
|
|
10
|
+
<article class="round border padding">
|
|
11
|
+
<h6 class="bold mb-1">{% trans "Orders Over Time" %}</h6>
|
|
12
|
+
<canvas id="orderChart" style="width:100%; max-height:300px;"></canvas>
|
|
13
|
+
</article>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="s12 m4">
|
|
16
|
+
<article class="round border padding">
|
|
17
|
+
<h6 class="bold mb-1">{% trans "Avg Rating" %}</h6>
|
|
18
|
+
<canvas id="ratingChart" style="width:100%; max-height:300px;"></canvas>
|
|
19
|
+
</article>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
(function() {
|
|
24
|
+
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#5d4037';
|
|
25
|
+
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary').trim() || '#ff7a18';
|
|
26
|
+
const tertiaryColor = getComputedStyle(document.documentElement).getPropertyValue('--tertiary').trim() || '#4caf50';
|
|
27
|
+
|
|
28
|
+
// Recipe Chart
|
|
29
|
+
new Chart(document.getElementById('recipeChart'), {
|
|
30
|
+
type: 'line',
|
|
31
|
+
data: {
|
|
32
|
+
labels: {{ recipe_labels|safe }},
|
|
33
|
+
datasets: [{
|
|
34
|
+
label: '{% trans "Recipes" %}',
|
|
35
|
+
data: {{ recipe_counts|safe }},
|
|
36
|
+
borderColor: primaryColor,
|
|
37
|
+
backgroundColor: primaryColor + '33',
|
|
38
|
+
fill: true,
|
|
39
|
+
tension: 0.4
|
|
40
|
+
}]
|
|
41
|
+
},
|
|
42
|
+
options: {
|
|
43
|
+
responsive: true,
|
|
44
|
+
plugins: { legend: { display: false } },
|
|
45
|
+
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Order Chart
|
|
50
|
+
new Chart(document.getElementById('orderChart'), {
|
|
51
|
+
type: 'line',
|
|
52
|
+
data: {
|
|
53
|
+
labels: {{ order_labels|safe }},
|
|
54
|
+
datasets: [{
|
|
55
|
+
label: '{% trans "Orders" %}',
|
|
56
|
+
data: {{ order_counts|safe }},
|
|
57
|
+
borderColor: tertiaryColor,
|
|
58
|
+
backgroundColor: tertiaryColor + '33',
|
|
59
|
+
fill: true,
|
|
60
|
+
tension: 0.4
|
|
61
|
+
}]
|
|
62
|
+
},
|
|
63
|
+
options: {
|
|
64
|
+
responsive: true,
|
|
65
|
+
plugins: { legend: { display: false } },
|
|
66
|
+
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Rating Chart
|
|
71
|
+
new Chart(document.getElementById('ratingChart'), {
|
|
72
|
+
type: 'bar',
|
|
73
|
+
data: {
|
|
74
|
+
labels: {{ rating_labels|safe }},
|
|
75
|
+
datasets: [{
|
|
76
|
+
label: '{% trans "Avg Rating" %}',
|
|
77
|
+
data: {{ rating_avgs|safe }},
|
|
78
|
+
backgroundColor: secondaryColor,
|
|
79
|
+
borderRadius: 4
|
|
80
|
+
}]
|
|
81
|
+
},
|
|
82
|
+
options: {
|
|
83
|
+
responsive: true,
|
|
84
|
+
plugins: { legend: { display: false } },
|
|
85
|
+
scales: { y: { min: 0, max: 10 } }
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
})();
|
|
89
|
+
</script>
|
|
90
|
+
</div>
|