sandwitches 2.3.0__py3-none-any.whl → 2.3.1__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/models.py +28 -1
- sandwitches/settings.py +1 -0
- sandwitches/tasks.py +74 -0
- sandwitches/templates/admin/admin_base.html +4 -0
- sandwitches/templates/admin/dashboard.html +125 -61
- 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/components/recipe_header.html +9 -0
- sandwitches/templates/components/side_menu.html +4 -0
- sandwitches/templates/profile.html +1 -1
- sandwitches/templates/recipe_form.html +15 -1
- sandwitches/urls.py +8 -0
- sandwitches/views.py +162 -21
- {sandwitches-2.3.0.dist-info → sandwitches-2.3.1.dist-info}/METADATA +1 -1
- {sandwitches-2.3.0.dist-info → sandwitches-2.3.1.dist-info}/RECORD +28 -20
- {sandwitches-2.3.0.dist-info → sandwitches-2.3.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
{% for order in orders %}
|
|
3
|
+
<tr>
|
|
4
|
+
<td>{{ order.id }}</td>
|
|
5
|
+
<td>
|
|
6
|
+
<div class="row align-center">
|
|
7
|
+
{% if order.user.avatar %}
|
|
8
|
+
<img src="{{ order.user.avatar.url }}" class="circle tiny">
|
|
9
|
+
{% else %}
|
|
10
|
+
<i class="circle tiny">person</i>
|
|
11
|
+
{% endif %}
|
|
12
|
+
<span class="max">{{ order.user.username }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
</td>
|
|
15
|
+
<td>{{ order.recipe.title }}</td>
|
|
16
|
+
<td>{{ order.total_price }} €</td>
|
|
17
|
+
<td>
|
|
18
|
+
<span class="chip {% if order.status == 'PENDING' %}surface-variant{% elif order.status == 'COMPLETED' %}primary{% else %}error{% endif %}">
|
|
19
|
+
{{ order.get_status_display }}
|
|
20
|
+
</span>
|
|
21
|
+
</td>
|
|
22
|
+
<td>{{ order.created_at|date:"d/m/Y H:i" }}</td>
|
|
23
|
+
</tr>
|
|
24
|
+
{% empty %}
|
|
25
|
+
<tr>
|
|
26
|
+
<td colspan="6" class="center-align">{% trans "No orders found." %}</td>
|
|
27
|
+
</tr>
|
|
28
|
+
{% endfor %}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<th>{% trans "Recipe" %}</th>
|
|
13
13
|
<th>{% trans "User" %}</th>
|
|
14
14
|
<th class="min center-align">{% trans "Score" %}</th>
|
|
15
|
+
<th>{% trans "Comment" %}</th>
|
|
15
16
|
<th>{% trans "Updated" %}</th>
|
|
16
17
|
<th class="right-align">{% trans "Actions" %}</th>
|
|
17
18
|
</tr>
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
<span>{{ r.score }}</span>
|
|
28
29
|
</div>
|
|
29
30
|
</td>
|
|
31
|
+
<td>{{ r.comment|default:"-"|truncatechars:50 }}</td>
|
|
30
32
|
<td>{{ r.updated_at|date:"SHORT_DATETIME_FORMAT" }}</td>
|
|
31
33
|
<td class="right-align">
|
|
32
34
|
<a href="{% url 'admin_rating_delete' r.pk %}" class="button circle transparent" title="{% trans 'Delete' %}"><i>delete</i></a>
|
|
@@ -60,6 +60,32 @@
|
|
|
60
60
|
<label>{{ form.uploaded_by.label }}</label>
|
|
61
61
|
{% if form.uploaded_by.errors %}<span class="error">{{ form.uploaded_by.errors|striptags }}</span>{% endif %}
|
|
62
62
|
</div>
|
|
63
|
+
|
|
64
|
+
<div class="field label border round mt-1">
|
|
65
|
+
{{ form.price }}
|
|
66
|
+
<label>{{ form.price.label }}</label>
|
|
67
|
+
{% if form.price.errors %}<span class="error">{{ form.price.errors|striptags }}</span>{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="field label border round mt-1">
|
|
71
|
+
{{ form.max_daily_orders }}
|
|
72
|
+
<label>{{ form.max_daily_orders.label }}</label>
|
|
73
|
+
{% if form.max_daily_orders.errors %}<span class="error">{{ form.max_daily_orders.errors|striptags }}</span>{% endif %}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="field middle-align mt-1">
|
|
77
|
+
<label class="checkbox">
|
|
78
|
+
{{ form.is_highlighted }}
|
|
79
|
+
<span>{{ form.is_highlighted.label }}</span>
|
|
80
|
+
</label>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="field middle-align mt-1">
|
|
84
|
+
<label class="checkbox">
|
|
85
|
+
{{ form.is_approved }}
|
|
86
|
+
<span>{{ form.is_approved.label }}</span>
|
|
87
|
+
</label>
|
|
88
|
+
</div>
|
|
63
89
|
</article>
|
|
64
90
|
|
|
65
91
|
<!-- Description -->
|
|
@@ -12,21 +12,48 @@
|
|
|
12
12
|
</a>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
<table class="border striped no-space">
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
<table class="border striped no-space">
|
|
16
|
+
<thead>
|
|
17
|
+
<tr>
|
|
18
|
+
<th class="min">{% trans "ID" %}</th>
|
|
19
|
+
<th class="min">{% trans "Image" %}</th>
|
|
20
|
+
<th class="max">
|
|
21
|
+
<a href="?sort={% if current_sort == 'title' %}-title{% else %}title{% endif %}" class="row align-center">
|
|
22
|
+
{% trans "Title" %}
|
|
23
|
+
{% if current_sort == 'title' %}<i>arrow_upward</i>{% elif current_sort == '-title' %}<i>arrow_downward</i>{% endif %}
|
|
24
|
+
</a>
|
|
25
|
+
</th>
|
|
26
|
+
<th class="min">
|
|
27
|
+
<a href="?sort={% if current_sort == 'price' %}-price{% else %}price{% endif %}" class="row align-center">
|
|
28
|
+
{% trans "Price" %}
|
|
29
|
+
{% if current_sort == 'price' %}<i>arrow_upward</i>{% elif current_sort == '-price' %}<i>arrow_downward</i>{% endif %}
|
|
30
|
+
</a>
|
|
31
|
+
</th>
|
|
32
|
+
<th class="min">
|
|
33
|
+
<a href="?sort={% if current_sort == 'orders' %}-orders{% else %}orders{% endif %}" class="row align-center">
|
|
34
|
+
{% trans "Orders" %}
|
|
35
|
+
{% if current_sort == 'orders' %}<i>arrow_upward</i>{% elif current_sort == '-orders' %}<i>arrow_downward</i>{% endif %}
|
|
36
|
+
</a>
|
|
37
|
+
</th>
|
|
38
|
+
<th class="min">{% trans "Rating" %}</th>
|
|
39
|
+
<th class="min">{% trans "Approved" %}</th>
|
|
40
|
+
<th class="m l">{% trans "Tags" %}</th>
|
|
41
|
+
<th>
|
|
42
|
+
<a href="?sort={% if current_sort == 'uploader' %}-uploader{% else %}uploader{% endif %}" class="row align-center">
|
|
43
|
+
{% trans "Uploader" %}
|
|
44
|
+
{% if current_sort == 'uploader' %}<i>arrow_upward</i>{% elif current_sort == '-uploader' %}<i>arrow_downward</i>{% endif %}
|
|
45
|
+
</a>
|
|
46
|
+
</th>
|
|
47
|
+
<th>
|
|
48
|
+
<a href="?sort={% if current_sort == 'created_at' %}-created_at{% else %}created_at{% endif %}" class="row align-center">
|
|
49
|
+
{% trans "Created" %}
|
|
50
|
+
{% if current_sort == 'created_at' %}<i>arrow_upward</i>{% elif current_sort == '-created_at' %}<i>arrow_downward</i>{% endif %}
|
|
51
|
+
</a>
|
|
52
|
+
</th>
|
|
53
|
+
<th class="right-align">{% trans "Actions" %}</th>
|
|
54
|
+
</tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody> {% for recipe in recipes %}
|
|
30
57
|
<tr class="pointer" onclick="location.href='{% url 'admin_recipe_edit' recipe.pk %}'">
|
|
31
58
|
<td class="min">{{ recipe.id }}</td>
|
|
32
59
|
<td class="min">
|
|
@@ -39,14 +66,37 @@
|
|
|
39
66
|
{% endif %}
|
|
40
67
|
</td>
|
|
41
68
|
<td class="max">
|
|
69
|
+
{% if recipe.is_highlighted %}
|
|
70
|
+
<i class="tiny amber-text">star</i>
|
|
71
|
+
{% endif %}
|
|
42
72
|
<b>{{ recipe.title }}</b>
|
|
43
73
|
</td>
|
|
74
|
+
<td class="min no-wrap">
|
|
75
|
+
{% if recipe.price %}
|
|
76
|
+
{{ recipe.price }} €
|
|
77
|
+
{% else %}
|
|
78
|
+
-
|
|
79
|
+
{% endif %}
|
|
80
|
+
</td>
|
|
81
|
+
<td class="min no-wrap">
|
|
82
|
+
{{ recipe.daily_orders_count }} / {{ recipe.max_daily_orders|default:"∞" }}
|
|
83
|
+
</td>
|
|
44
84
|
<td class="min center-align">
|
|
45
85
|
<div class="row align-center">
|
|
46
86
|
<i class="primary-text">star</i>
|
|
47
87
|
<span>{{ recipe.avg_rating|default:0|floatformat:1 }}</span>
|
|
48
88
|
</div>
|
|
49
89
|
</td>
|
|
90
|
+
<td class="min center-align">
|
|
91
|
+
{% if recipe.is_approved %}
|
|
92
|
+
<i class="primary-text">check_circle</i>
|
|
93
|
+
{% else %}
|
|
94
|
+
<a href="{% url 'admin_recipe_approve' recipe.pk %}" class="button tiny primary round" onclick="event.stopPropagation();">
|
|
95
|
+
<i>check</i>
|
|
96
|
+
<span>{% trans "Approve" %}</span>
|
|
97
|
+
</a>
|
|
98
|
+
{% endif %}
|
|
99
|
+
</td>
|
|
50
100
|
<td class="m l">
|
|
51
101
|
{% for tag in recipe.tags.all %}
|
|
52
102
|
<span class="chip tiny">{{ tag.name }}</span>
|
sandwitches/templates/base.html
CHANGED
|
@@ -56,6 +56,18 @@
|
|
|
56
56
|
</svg>
|
|
57
57
|
</div>
|
|
58
58
|
{% block navbar %}{% endblock %}
|
|
59
|
+
|
|
60
|
+
{% if messages %}
|
|
61
|
+
<div class="padding no-margin">
|
|
62
|
+
{% for message in messages %}
|
|
63
|
+
<div class="snackbar active {% if message.tags == 'error' %}error{% else %}primary{% endif %}">
|
|
64
|
+
<i>{% if message.tags == 'error' %}error{% else %}info{% endif %}</i>
|
|
65
|
+
<span>{{ message }}</span>
|
|
66
|
+
</div>
|
|
67
|
+
{% endfor %}
|
|
68
|
+
</div>
|
|
69
|
+
{% endif %}
|
|
70
|
+
|
|
59
71
|
<main class="container" role="main">{% block content %}{% endblock %}</main>
|
|
60
72
|
{% block footer %}{% endblock %}
|
|
61
73
|
{% block extra_scripts %}{% endblock %}
|
|
@@ -36,5 +36,14 @@
|
|
|
36
36
|
<div class="row align-center">
|
|
37
37
|
<i class="primary-text">euro_symbol</i>
|
|
38
38
|
<h5 class="bold ml-1">{{ recipe.price }}</h5>
|
|
39
|
+
{% if user.is_authenticated %}
|
|
40
|
+
<form action="{% url 'order_recipe' recipe.pk %}" method="post" class="ml-2">
|
|
41
|
+
{% csrf_token %}
|
|
42
|
+
<button type="submit" class="button primary round">
|
|
43
|
+
<i>shopping_cart</i>
|
|
44
|
+
<span>{% trans "Order Now" %}</span>
|
|
45
|
+
</button>
|
|
46
|
+
</form>
|
|
47
|
+
{% endif %}
|
|
39
48
|
</div>
|
|
40
49
|
{% endif %}
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
<i class="extra padding">favorite</i>
|
|
16
16
|
<span class="large-text">{% trans "Favorites" %}</span>
|
|
17
17
|
</a>
|
|
18
|
+
<a href="{% url 'submit_recipe' %}" class="padding {% if request.resolver_match.url_name == 'submit_recipe' %}active{% endif %}">
|
|
19
|
+
<i class="extra padding">add_circle</i>
|
|
20
|
+
<span class="large-text">{% trans "Submit Recipe" %}</span>
|
|
21
|
+
</a>
|
|
18
22
|
{% endif %}
|
|
19
23
|
<a href="/api/docs" class="padding">
|
|
20
24
|
<i class="extra padding">api</i>
|
|
@@ -68,9 +68,23 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
</div>
|
|
70
70
|
|
|
71
|
+
<div class="s12 m6">
|
|
72
|
+
<div class="field label border round">
|
|
73
|
+
{{ form.price }}
|
|
74
|
+
<label>{% trans "Price" %}</label>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="s12 m6">
|
|
79
|
+
<div class="field label border round">
|
|
80
|
+
{{ form.servings }}
|
|
81
|
+
<label>{% trans "Servings" %}</label>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
71
85
|
<div class="s12">
|
|
72
86
|
<div class="field label border round">
|
|
73
|
-
{{ form.
|
|
87
|
+
{{ form.tags_string }}
|
|
74
88
|
<label>{% trans "Tags (comma separated)" %}</label>
|
|
75
89
|
</div>
|
|
76
90
|
</div>
|
sandwitches/urls.py
CHANGED
|
@@ -34,6 +34,7 @@ urlpatterns = [
|
|
|
34
34
|
path("login/", views.CustomLoginView.as_view(), name="login"),
|
|
35
35
|
path("logout/", LogoutView.as_view(next_page="index"), name="logout"),
|
|
36
36
|
path("profile/", views.user_profile, name="user_profile"),
|
|
37
|
+
path("submit-recipe/", views.submit_recipe, name="submit_recipe"),
|
|
37
38
|
path("admin/", admin.site.urls),
|
|
38
39
|
path("api/", api.urls),
|
|
39
40
|
path("media/<path:file_path>", views.media, name="media"),
|
|
@@ -49,6 +50,7 @@ urlpatterns += i18n_patterns(
|
|
|
49
50
|
path("recipes/<slug:slug>/", views.recipe_detail, name="recipe_detail"),
|
|
50
51
|
path("setup/", views.setup, name="setup"),
|
|
51
52
|
path("recipes/<int:pk>/rate/", views.recipe_rate, name="recipe_rate"),
|
|
53
|
+
path("recipes/<int:pk>/order/", views.order_recipe, name="order_recipe"),
|
|
52
54
|
path("recipes/<int:pk>/favorite/", views.toggle_favorite, name="toggle_favorite"),
|
|
53
55
|
path("dashboard/", views.admin_dashboard, name="admin_dashboard"),
|
|
54
56
|
path("dashboard/recipes/", views.admin_recipe_list, name="admin_recipe_list"),
|
|
@@ -63,6 +65,11 @@ urlpatterns += i18n_patterns(
|
|
|
63
65
|
views.admin_recipe_delete,
|
|
64
66
|
name="admin_recipe_delete",
|
|
65
67
|
),
|
|
68
|
+
path(
|
|
69
|
+
"dashboard/recipes/<int:pk>/approve/",
|
|
70
|
+
views.admin_recipe_approve,
|
|
71
|
+
name="admin_recipe_approve",
|
|
72
|
+
),
|
|
66
73
|
path(
|
|
67
74
|
"dashboard/recipes/<int:pk>/rotate/",
|
|
68
75
|
views.admin_recipe_rotate,
|
|
@@ -99,6 +106,7 @@ urlpatterns += i18n_patterns(
|
|
|
99
106
|
views.admin_rating_delete,
|
|
100
107
|
name="admin_rating_delete",
|
|
101
108
|
),
|
|
109
|
+
path("dashboard/orders/", views.admin_order_list, name="admin_order_list"),
|
|
102
110
|
prefix_default_language=True,
|
|
103
111
|
)
|
|
104
112
|
|
sandwitches/views.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
1
3
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
2
4
|
from django.urls import reverse
|
|
3
5
|
from django.contrib import messages
|
|
@@ -6,7 +8,7 @@ from django.contrib.auth import get_user_model
|
|
|
6
8
|
from django.contrib.auth.decorators import login_required
|
|
7
9
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
8
10
|
from django.utils.translation import gettext as _
|
|
9
|
-
from .models import Recipe, Rating, Tag
|
|
11
|
+
from .models import Recipe, Rating, Tag, Order
|
|
10
12
|
from .forms import (
|
|
11
13
|
RecipeForm,
|
|
12
14
|
AdminSetupForm,
|
|
@@ -15,10 +17,11 @@ from .forms import (
|
|
|
15
17
|
UserEditForm,
|
|
16
18
|
TagForm,
|
|
17
19
|
UserProfileForm,
|
|
20
|
+
UserRecipeSubmissionForm,
|
|
18
21
|
)
|
|
19
|
-
from django.http import HttpResponseBadRequest
|
|
22
|
+
from django.http import HttpResponseBadRequest, Http404
|
|
20
23
|
from django.conf import settings
|
|
21
|
-
from django.http import FileResponse
|
|
24
|
+
from django.http import FileResponse
|
|
22
25
|
from pathlib import Path
|
|
23
26
|
import mimetypes
|
|
24
27
|
from PIL import Image
|
|
@@ -32,6 +35,30 @@ from sandwitches import __version__ as sandwitches_version
|
|
|
32
35
|
User = get_user_model()
|
|
33
36
|
|
|
34
37
|
|
|
38
|
+
@login_required
|
|
39
|
+
def submit_recipe(request):
|
|
40
|
+
if request.method == "POST":
|
|
41
|
+
form = UserRecipeSubmissionForm(request.POST, request.FILES)
|
|
42
|
+
if form.is_valid():
|
|
43
|
+
recipe = form.save(commit=False)
|
|
44
|
+
recipe.uploaded_by = request.user
|
|
45
|
+
recipe.is_approved = False # Explicitly set to False just in case
|
|
46
|
+
recipe.save()
|
|
47
|
+
form.save_m2m()
|
|
48
|
+
messages.success(
|
|
49
|
+
request,
|
|
50
|
+
_("Your recipe has been submitted and is awaiting admin approval."),
|
|
51
|
+
)
|
|
52
|
+
return redirect("user_profile")
|
|
53
|
+
else:
|
|
54
|
+
form = UserRecipeSubmissionForm()
|
|
55
|
+
return render(
|
|
56
|
+
request,
|
|
57
|
+
"recipe_form.html",
|
|
58
|
+
{"form": form, "title": _("Submit Recipe"), "version": sandwitches_version},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
35
62
|
class CustomLoginView(LoginView):
|
|
36
63
|
template_name = "login.html"
|
|
37
64
|
redirect_authenticated_user = True
|
|
@@ -88,6 +115,15 @@ def admin_dashboard(request):
|
|
|
88
115
|
.order_by("date")
|
|
89
116
|
)
|
|
90
117
|
|
|
118
|
+
# Orders over time
|
|
119
|
+
order_data = (
|
|
120
|
+
Order.objects.filter(created_at__date__range=(start_date, end_date)) # ty:ignore[unresolved-attribute]
|
|
121
|
+
.annotate(date=TruncDate("created_at"))
|
|
122
|
+
.values("date")
|
|
123
|
+
.annotate(count=Count("id"))
|
|
124
|
+
.order_by("date")
|
|
125
|
+
)
|
|
126
|
+
|
|
91
127
|
# Prepare labels and data for JS
|
|
92
128
|
recipe_labels = [d["date"].strftime("%d/%m/%Y") for d in recipe_data]
|
|
93
129
|
recipe_counts = [d["count"] for d in recipe_data]
|
|
@@ -95,36 +131,71 @@ def admin_dashboard(request):
|
|
|
95
131
|
rating_labels = [d["date"].strftime("%d/%m/%Y") for d in rating_data]
|
|
96
132
|
rating_avgs = [float(d["avg"]) for d in rating_data]
|
|
97
133
|
|
|
134
|
+
order_labels = [d["date"].strftime("%d/%m/%Y") for d in order_data]
|
|
135
|
+
order_counts = [d["count"] for d in order_data]
|
|
136
|
+
|
|
137
|
+
pending_recipes = Recipe.objects.filter(is_approved=False).order_by("-created_at") # ty:ignore[unresolved-attribute]
|
|
138
|
+
|
|
139
|
+
context = {
|
|
140
|
+
"recipe_count": recipe_count,
|
|
141
|
+
"user_count": user_count,
|
|
142
|
+
"tag_count": tag_count,
|
|
143
|
+
"recent_recipes": recent_recipes,
|
|
144
|
+
"pending_recipes": pending_recipes,
|
|
145
|
+
"recipe_labels": recipe_labels,
|
|
146
|
+
"recipe_counts": recipe_counts,
|
|
147
|
+
"rating_labels": rating_labels,
|
|
148
|
+
"rating_avgs": rating_avgs,
|
|
149
|
+
"order_labels": order_labels,
|
|
150
|
+
"order_counts": order_counts,
|
|
151
|
+
"start_date": start_date.strftime("%Y-%m-%d"),
|
|
152
|
+
"end_date": end_date.strftime("%Y-%m-%d"),
|
|
153
|
+
"version": sandwitches_version,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if request.headers.get("HX-Request"):
|
|
157
|
+
return render(request, "admin/partials/dashboard_charts.html", context)
|
|
158
|
+
|
|
98
159
|
return render(
|
|
99
160
|
request,
|
|
100
161
|
"admin/dashboard.html",
|
|
101
|
-
|
|
102
|
-
"recipe_count": recipe_count,
|
|
103
|
-
"user_count": user_count,
|
|
104
|
-
"tag_count": tag_count,
|
|
105
|
-
"recent_recipes": recent_recipes,
|
|
106
|
-
"recipe_labels": recipe_labels,
|
|
107
|
-
"recipe_counts": recipe_counts,
|
|
108
|
-
"rating_labels": rating_labels,
|
|
109
|
-
"rating_avgs": rating_avgs,
|
|
110
|
-
"start_date": start_date.strftime("%Y-%m-%d"),
|
|
111
|
-
"end_date": end_date.strftime("%Y-%m-%d"),
|
|
112
|
-
"version": sandwitches_version,
|
|
113
|
-
},
|
|
162
|
+
context,
|
|
114
163
|
)
|
|
115
164
|
|
|
116
165
|
|
|
117
166
|
@staff_member_required
|
|
118
167
|
def admin_recipe_list(request):
|
|
168
|
+
sort_param = request.GET.get("sort", "-created_at")
|
|
169
|
+
allowed_sorts = {
|
|
170
|
+
"title": "title",
|
|
171
|
+
"-title": "-title",
|
|
172
|
+
"created_at": "created_at",
|
|
173
|
+
"-created_at": "-created_at",
|
|
174
|
+
"uploader": "uploaded_by__username",
|
|
175
|
+
"-uploader": "-uploaded_by__username",
|
|
176
|
+
"price": "price",
|
|
177
|
+
"-price": "-price",
|
|
178
|
+
"orders": "daily_orders_count",
|
|
179
|
+
"-orders": "-daily_orders_count",
|
|
180
|
+
"rating": "avg_rating",
|
|
181
|
+
"-rating": "-avg_rating",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
order_by = allowed_sorts.get(sort_param, "-created_at")
|
|
185
|
+
|
|
119
186
|
recipes = (
|
|
120
187
|
Recipe.objects.annotate(avg_rating=Avg("ratings__score")) # ty:ignore[unresolved-attribute]
|
|
121
188
|
.prefetch_related("tags")
|
|
122
|
-
.
|
|
189
|
+
.order_by(order_by)
|
|
123
190
|
)
|
|
124
191
|
return render(
|
|
125
192
|
request,
|
|
126
193
|
"admin/recipe_list.html",
|
|
127
|
-
{
|
|
194
|
+
{
|
|
195
|
+
"recipes": recipes,
|
|
196
|
+
"version": sandwitches_version,
|
|
197
|
+
"current_sort": sort_param,
|
|
198
|
+
},
|
|
128
199
|
)
|
|
129
200
|
|
|
130
201
|
|
|
@@ -171,6 +242,17 @@ def admin_recipe_edit(request, pk):
|
|
|
171
242
|
)
|
|
172
243
|
|
|
173
244
|
|
|
245
|
+
@staff_member_required
|
|
246
|
+
def admin_recipe_approve(request, pk):
|
|
247
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
248
|
+
recipe.is_approved = True
|
|
249
|
+
recipe.save()
|
|
250
|
+
messages.success(
|
|
251
|
+
request, _("Recipe '%(title)s' approved.") % {"title": recipe.title}
|
|
252
|
+
)
|
|
253
|
+
return redirect("admin_recipe_list")
|
|
254
|
+
|
|
255
|
+
|
|
174
256
|
@staff_member_required
|
|
175
257
|
def admin_recipe_delete(request, pk):
|
|
176
258
|
recipe = get_object_or_404(Recipe, pk=pk)
|
|
@@ -366,8 +448,38 @@ def admin_rating_delete(request, pk):
|
|
|
366
448
|
)
|
|
367
449
|
|
|
368
450
|
|
|
451
|
+
@staff_member_required
|
|
452
|
+
def admin_order_list(request):
|
|
453
|
+
orders = (
|
|
454
|
+
Order.objects.select_related("user", "recipe") # ty:ignore[unresolved-attribute]
|
|
455
|
+
.all()
|
|
456
|
+
.order_by("-created_at")
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if request.headers.get("HX-Request"):
|
|
460
|
+
return render(
|
|
461
|
+
request,
|
|
462
|
+
"admin/partials/order_rows.html",
|
|
463
|
+
{"orders": orders, "version": sandwitches_version},
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
return render(
|
|
467
|
+
request,
|
|
468
|
+
"admin/order_list.html",
|
|
469
|
+
{"orders": orders, "version": sandwitches_version},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
369
473
|
def recipe_detail(request, slug):
|
|
370
474
|
recipe = get_object_or_404(Recipe, slug=slug)
|
|
475
|
+
|
|
476
|
+
if not recipe.is_approved:
|
|
477
|
+
if not (
|
|
478
|
+
request.user.is_authenticated
|
|
479
|
+
and (request.user.is_staff or recipe.uploaded_by == request.user)
|
|
480
|
+
):
|
|
481
|
+
raise Http404("Recipe not found or pending approval.")
|
|
482
|
+
|
|
371
483
|
avg = recipe.average_rating()
|
|
372
484
|
count = recipe.rating_count()
|
|
373
485
|
user_rating = None
|
|
@@ -401,6 +513,28 @@ def recipe_detail(request, slug):
|
|
|
401
513
|
)
|
|
402
514
|
|
|
403
515
|
|
|
516
|
+
@login_required
|
|
517
|
+
def order_recipe(request, pk):
|
|
518
|
+
"""
|
|
519
|
+
Create an order for the given recipe by the logged-in user.
|
|
520
|
+
"""
|
|
521
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
522
|
+
if request.method != "POST":
|
|
523
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
order = Order.objects.create(user=request.user, recipe=recipe) # ty:ignore[unresolved-attribute]
|
|
527
|
+
logging.debug(f"Created {order}")
|
|
528
|
+
messages.success(
|
|
529
|
+
request,
|
|
530
|
+
_("Your order for %(title)s has been submitted!") % {"title": recipe.title},
|
|
531
|
+
)
|
|
532
|
+
except (ValidationError, ValueError) as e:
|
|
533
|
+
messages.error(request, str(e))
|
|
534
|
+
|
|
535
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
536
|
+
|
|
537
|
+
|
|
404
538
|
@login_required
|
|
405
539
|
def recipe_rate(request, pk):
|
|
406
540
|
"""
|
|
@@ -510,9 +644,16 @@ def favorites(request):
|
|
|
510
644
|
def index(request):
|
|
511
645
|
if not User.objects.filter(is_superuser=True).exists():
|
|
512
646
|
return redirect("setup")
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
647
|
+
|
|
648
|
+
recipes = Recipe.objects.all().prefetch_related("favorited_by") # ty:ignore[unresolved-attribute]
|
|
649
|
+
|
|
650
|
+
if not (request.user.is_authenticated and request.user.is_staff):
|
|
651
|
+
if request.user.is_authenticated:
|
|
652
|
+
# Show approved recipes OR recipes uploaded by the current user
|
|
653
|
+
recipes = recipes.filter(Q(is_approved=True) | Q(uploaded_by=request.user))
|
|
654
|
+
else:
|
|
655
|
+
# Show only approved recipes for anonymous users
|
|
656
|
+
recipes = recipes.filter(is_approved=True)
|
|
516
657
|
|
|
517
658
|
# Filtering
|
|
518
659
|
q = request.GET.get("q")
|