sandwitches 2.3.0__py3-none-any.whl → 2.3.2__py3-none-any.whl
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/api.py +22 -1
- sandwitches/forms.py +36 -0
- sandwitches/management/__init__.py +0 -0
- sandwitches/management/commands/__init__.py +0 -0
- sandwitches/management/commands/reset_daily_orders.py +14 -0
- sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py +36 -0
- sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py +22 -0
- sandwitches/migrations/0010_rename_is_approved_historicalrecipe_is_community_made_and_more.py +22 -0
- sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py +25 -0
- sandwitches/models.py +28 -1
- sandwitches/settings.py +1 -0
- sandwitches/tasks.py +74 -0
- sandwitches/templates/admin/admin_base.html +8 -4
- sandwitches/templates/admin/dashboard.html +132 -69
- sandwitches/templates/admin/order_list.html +30 -0
- sandwitches/templates/admin/partials/dashboard_charts.html +90 -0
- sandwitches/templates/admin/partials/order_rows.html +28 -0
- sandwitches/templates/admin/rating_list.html +2 -0
- sandwitches/templates/admin/recipe_form.html +26 -0
- sandwitches/templates/admin/recipe_list.html +65 -15
- sandwitches/templates/base.html +12 -0
- sandwitches/templates/{recipe_form.html → community.html} +30 -8
- sandwitches/templates/components/recipe_header.html +9 -0
- sandwitches/templates/components/side_menu.html +4 -0
- sandwitches/templates/profile.html +1 -1
- sandwitches/urls.py +8 -0
- sandwitches/views.py +178 -21
- {sandwitches-2.3.0.dist-info → sandwitches-2.3.2.dist-info}/METADATA +1 -1
- {sandwitches-2.3.0.dist-info → sandwitches-2.3.2.dist-info}/RECORD +30 -20
- {sandwitches-2.3.0.dist-info → sandwitches-2.3.2.dist-info}/WHEEL +0 -0
sandwitches/api.py
CHANGED
|
@@ -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)}
|
sandwitches/forms.py
CHANGED
|
@@ -137,6 +137,9 @@ class RecipeForm(forms.ModelForm):
|
|
|
137
137
|
"ingredients",
|
|
138
138
|
"instructions",
|
|
139
139
|
"price",
|
|
140
|
+
"is_highlighted",
|
|
141
|
+
"is_community_made",
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-01-24 15:32
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0009_historicalrecipe_is_approved_recipe_is_approved"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.RenameField(
|
|
13
|
+
model_name="historicalrecipe",
|
|
14
|
+
old_name="is_approved",
|
|
15
|
+
new_name="is_community_made",
|
|
16
|
+
),
|
|
17
|
+
migrations.RenameField(
|
|
18
|
+
model_name="recipe",
|
|
19
|
+
old_name="is_approved",
|
|
20
|
+
new_name="is_community_made",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-01-24 16:01
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
(
|
|
9
|
+
"sandwitches",
|
|
10
|
+
"0010_rename_is_approved_historicalrecipe_is_community_made_and_more",
|
|
11
|
+
),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AlterField(
|
|
16
|
+
model_name="historicalrecipe",
|
|
17
|
+
name="is_community_made",
|
|
18
|
+
field=models.BooleanField(default=False),
|
|
19
|
+
),
|
|
20
|
+
migrations.AlterField(
|
|
21
|
+
model_name="recipe",
|
|
22
|
+
name="is_community_made",
|
|
23
|
+
field=models.BooleanField(default=False),
|
|
24
|
+
),
|
|
25
|
+
]
|
sandwitches/models.py
CHANGED
|
@@ -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_community_made = models.BooleanField(default=False)
|
|
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}"
|
sandwitches/settings.py
CHANGED
sandwitches/tasks.py
CHANGED
|
@@ -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
|
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
{% load i18n static %}
|
|
3
3
|
|
|
4
4
|
{% block extra_head %}
|
|
5
|
-
<link href="
|
|
6
|
-
<script
|
|
7
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/material-dynamic-colors@1.1.2/dist/cdn/material-dynamic-colors.min.js"></script>
|
|
8
|
-
<script src="https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
|
5
|
+
<link href="{% static "dist/main.css" %}" rel="stylesheet">
|
|
6
|
+
<script src="{% static "dist/main.js" %}" defer></script>
|
|
9
7
|
<link rel="icon" type="image/svg+xml" href="{% static "icons/favicon.svg" %}">
|
|
10
8
|
<style>
|
|
11
9
|
main.container {
|
|
12
10
|
padding-top: 2rem;
|
|
13
11
|
padding-bottom: 2rem;
|
|
12
|
+
padding-left: 5%;
|
|
13
|
+
padding-right: 5%;
|
|
14
14
|
}
|
|
15
15
|
.admin-thumb {
|
|
16
16
|
width: 80px;
|
|
@@ -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>
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{% extends "admin/admin_base.html" %}
|
|
2
|
-
{% load i18n %}
|
|
2
|
+
{% load i18n static %}
|
|
3
3
|
|
|
4
4
|
{% block admin_title %}{% trans "Dashboard" %}{% endblock %}
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
{% block extra_head %}
|
|
7
|
+
{{ block.super }}
|
|
8
|
+
<link href="{% static "dist/main.css" %}" rel="stylesheet">
|
|
9
|
+
<link href="{% static "main.css" %}" rel="stylesheet">
|
|
10
|
+
<script src="{% static "dist/main.js" %}" defer></script>
|
|
11
|
+
{% endblock %}
|
|
7
12
|
|
|
8
13
|
{% block content %}
|
|
9
14
|
|
|
@@ -127,37 +132,147 @@
|
|
|
127
132
|
|
|
128
133
|
|
|
129
134
|
|
|
130
|
-
|
|
135
|
+
<!-- Charts Section -->
|
|
131
136
|
|
|
132
|
-
<div class="s12 m6">
|
|
133
137
|
|
|
134
|
-
<article class="round border padding">
|
|
135
138
|
|
|
136
|
-
<
|
|
139
|
+
<div class="s12">
|
|
137
140
|
|
|
138
|
-
<canvas id="recipeChart" style="width:100%; max-height:300px;"></canvas>
|
|
139
141
|
|
|
140
|
-
</article>
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
{% include "admin/partials/dashboard_charts.html" %}
|
|
143
144
|
|
|
144
|
-
<div class="s12 m6">
|
|
145
145
|
|
|
146
|
-
<article class="round border padding">
|
|
147
146
|
|
|
148
|
-
|
|
147
|
+
</div>
|
|
149
148
|
|
|
150
|
-
<canvas id="ratingChart" style="width:100%; max-height:300px;"></canvas>
|
|
151
149
|
|
|
152
|
-
</article>
|
|
153
150
|
|
|
154
|
-
</div>
|
|
155
151
|
|
|
156
152
|
|
|
157
153
|
|
|
158
|
-
<div class="s12">
|
|
159
154
|
|
|
160
|
-
|
|
155
|
+
{% if pending_recipes %}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
<div class="s12">
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
<h5 class="bold error-text">{% trans "Pending Approvals" %}</h5>
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
<table class="border striped no-space">
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
<thead>
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
<tr>
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
<th>{% trans "Title" %}</th>
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
<th>{% trans "Uploader" %}</th>
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
<th>{% trans "Created At" %}</th>
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
<th class="right-align">{% trans "Actions" %}</th>
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
</tr>
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
</thead>
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
<tbody>
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
{% for recipe in pending_recipes %}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
<tr class="pointer" onclick="location.href='{% url 'admin_recipe_edit' recipe.pk %}'">
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
<td>{{ recipe.title }}</td>
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
<td>{{ recipe.uploaded_by.username|default:"-" }}</td>
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
<td>{{ recipe.created_at|date:"SHORT_DATETIME_FORMAT" }}</td>
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
<td class="right-align">
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
<a href="{% url 'admin_recipe_approve' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();" title="{% trans 'Approve' %}"><i>check</i></a>
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
<a href="{% url 'admin_recipe_edit' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();"><i>edit</i></a>
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
</td>
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
</tr>
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
{% endfor %}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
</tbody>
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
</table>
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
{% endif %}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
<div class="s12">
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
<h5 class="bold">{% trans "Recent Recipes" %}</h5>
|
|
161
276
|
|
|
162
277
|
<table class="border striped no-space">
|
|
163
278
|
|
|
@@ -208,55 +323,3 @@
|
|
|
208
323
|
</div>
|
|
209
324
|
|
|
210
325
|
{% endblock %}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{% block admin_scripts %}
|
|
215
|
-
<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
|
-
{% 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 %}
|