sandwitches 2.2.0__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/__init__.py +6 -0
- sandwitches/admin.py +69 -0
- sandwitches/api.py +207 -0
- sandwitches/asgi.py +16 -0
- sandwitches/feeds.py +23 -0
- sandwitches/forms.py +196 -0
- sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
- sandwitches/migrations/0001_initial.py +328 -0
- sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
- sandwitches/migrations/0003_setting.py +35 -0
- sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
- sandwitches/migrations/0005_rating_comment.py +17 -0
- sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +22 -0
- sandwitches/migrations/__init__.py +0 -0
- sandwitches/models.py +218 -0
- sandwitches/settings.py +220 -0
- sandwitches/storage.py +114 -0
- sandwitches/tasks.py +115 -0
- sandwitches/templates/admin/admin_base.html +118 -0
- sandwitches/templates/admin/confirm_delete.html +23 -0
- sandwitches/templates/admin/dashboard.html +262 -0
- sandwitches/templates/admin/rating_list.html +38 -0
- sandwitches/templates/admin/recipe_form.html +184 -0
- sandwitches/templates/admin/recipe_list.html +64 -0
- sandwitches/templates/admin/tag_form.html +30 -0
- sandwitches/templates/admin/tag_list.html +37 -0
- sandwitches/templates/admin/task_detail.html +91 -0
- sandwitches/templates/admin/task_list.html +41 -0
- sandwitches/templates/admin/user_form.html +37 -0
- sandwitches/templates/admin/user_list.html +60 -0
- sandwitches/templates/base.html +94 -0
- sandwitches/templates/base_beer.html +57 -0
- sandwitches/templates/components/carousel_scripts.html +59 -0
- sandwitches/templates/components/favorites_search_form.html +85 -0
- sandwitches/templates/components/footer.html +14 -0
- sandwitches/templates/components/ingredients_scripts.html +50 -0
- sandwitches/templates/components/ingredients_section.html +11 -0
- sandwitches/templates/components/instructions_section.html +9 -0
- sandwitches/templates/components/language_dialog.html +26 -0
- sandwitches/templates/components/navbar.html +27 -0
- sandwitches/templates/components/rating_section.html +66 -0
- sandwitches/templates/components/recipe_header.html +32 -0
- sandwitches/templates/components/search_form.html +106 -0
- sandwitches/templates/components/search_scripts.html +98 -0
- sandwitches/templates/components/side_menu.html +35 -0
- sandwitches/templates/components/user_menu.html +10 -0
- sandwitches/templates/detail.html +178 -0
- sandwitches/templates/favorites.html +42 -0
- sandwitches/templates/index.html +76 -0
- sandwitches/templates/login.html +57 -0
- sandwitches/templates/partials/recipe_list.html +87 -0
- sandwitches/templates/recipe_form.html +119 -0
- sandwitches/templates/setup.html +105 -0
- sandwitches/templates/signup.html +133 -0
- sandwitches/templatetags/__init__.py +0 -0
- sandwitches/templatetags/custom_filters.py +15 -0
- sandwitches/templatetags/markdown_extras.py +17 -0
- sandwitches/urls.py +109 -0
- sandwitches/utils.py +222 -0
- sandwitches/views.py +647 -0
- sandwitches/wsgi.py +16 -0
- sandwitches-2.2.0.dist-info/METADATA +104 -0
- sandwitches-2.2.0.dist-info/RECORD +65 -0
- sandwitches-2.2.0.dist-info/WHEEL +4 -0
sandwitches/views.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
from django.shortcuts import render, get_object_or_404, redirect
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
from django.contrib import messages
|
|
4
|
+
from django.contrib.auth import login
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.contrib.auth.decorators import login_required
|
|
7
|
+
from django.contrib.admin.views.decorators import staff_member_required
|
|
8
|
+
from django.utils.translation import gettext as _
|
|
9
|
+
from .models import Recipe, Rating, Tag
|
|
10
|
+
from .forms import (
|
|
11
|
+
RecipeForm,
|
|
12
|
+
AdminSetupForm,
|
|
13
|
+
UserSignupForm,
|
|
14
|
+
RatingForm,
|
|
15
|
+
UserEditForm,
|
|
16
|
+
TagForm,
|
|
17
|
+
)
|
|
18
|
+
from django.http import HttpResponseBadRequest
|
|
19
|
+
from django.conf import settings
|
|
20
|
+
from django.http import FileResponse, Http404
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import mimetypes
|
|
23
|
+
from PIL import Image
|
|
24
|
+
from django.db.models import Q, Avg
|
|
25
|
+
from django_tasks.backends.database.models import DBTaskResult
|
|
26
|
+
from django.contrib.auth.views import LoginView
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from sandwitches import __version__ as sandwitches_version
|
|
30
|
+
|
|
31
|
+
User = get_user_model()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CustomLoginView(LoginView):
|
|
35
|
+
template_name = "login.html"
|
|
36
|
+
redirect_authenticated_user = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@staff_member_required
|
|
40
|
+
def admin_dashboard(request):
|
|
41
|
+
recipe_count = Recipe.objects.count() # ty:ignore[unresolved-attribute]
|
|
42
|
+
user_count = User.objects.count()
|
|
43
|
+
tag_count = Tag.objects.count() # ty:ignore[unresolved-attribute]
|
|
44
|
+
recent_recipes = Recipe.objects.order_by("-created_at")[:5] # ty:ignore[unresolved-attribute]
|
|
45
|
+
|
|
46
|
+
# Data for charts
|
|
47
|
+
from datetime import timedelta
|
|
48
|
+
from django.utils import timezone
|
|
49
|
+
from django.db.models.functions import TruncDate
|
|
50
|
+
from django.db.models import Count
|
|
51
|
+
|
|
52
|
+
# Get date range from request or default to last 30 days
|
|
53
|
+
end_date_str = request.GET.get("end_date")
|
|
54
|
+
start_date_str = request.GET.get("start_date")
|
|
55
|
+
|
|
56
|
+
today = timezone.now().date()
|
|
57
|
+
try:
|
|
58
|
+
end_date = (
|
|
59
|
+
timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
60
|
+
if end_date_str
|
|
61
|
+
else today
|
|
62
|
+
)
|
|
63
|
+
start_date = (
|
|
64
|
+
timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
65
|
+
if start_date_str
|
|
66
|
+
else end_date - timedelta(days=30)
|
|
67
|
+
)
|
|
68
|
+
except (ValueError, TypeError):
|
|
69
|
+
end_date = today
|
|
70
|
+
start_date = today - timedelta(days=30)
|
|
71
|
+
|
|
72
|
+
# Recipes over time
|
|
73
|
+
recipe_data = (
|
|
74
|
+
Recipe.objects.filter(created_at__date__range=(start_date, end_date)) # ty:ignore[unresolved-attribute]
|
|
75
|
+
.annotate(date=TruncDate("created_at"))
|
|
76
|
+
.values("date")
|
|
77
|
+
.annotate(count=Count("id"))
|
|
78
|
+
.order_by("date")
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Ratings over time
|
|
82
|
+
rating_data = (
|
|
83
|
+
Rating.objects.filter(created_at__date__range=(start_date, end_date)) # ty:ignore[unresolved-attribute]
|
|
84
|
+
.annotate(date=TruncDate("created_at"))
|
|
85
|
+
.values("date")
|
|
86
|
+
.annotate(avg=Avg("score"))
|
|
87
|
+
.order_by("date")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Prepare labels and data for JS
|
|
91
|
+
recipe_labels = [d["date"].strftime("%d/%m/%Y") for d in recipe_data]
|
|
92
|
+
recipe_counts = [d["count"] for d in recipe_data]
|
|
93
|
+
|
|
94
|
+
rating_labels = [d["date"].strftime("%d/%m/%Y") for d in rating_data]
|
|
95
|
+
rating_avgs = [float(d["avg"]) for d in rating_data]
|
|
96
|
+
|
|
97
|
+
return render(
|
|
98
|
+
request,
|
|
99
|
+
"admin/dashboard.html",
|
|
100
|
+
{
|
|
101
|
+
"recipe_count": recipe_count,
|
|
102
|
+
"user_count": user_count,
|
|
103
|
+
"tag_count": tag_count,
|
|
104
|
+
"recent_recipes": recent_recipes,
|
|
105
|
+
"recipe_labels": recipe_labels,
|
|
106
|
+
"recipe_counts": recipe_counts,
|
|
107
|
+
"rating_labels": rating_labels,
|
|
108
|
+
"rating_avgs": rating_avgs,
|
|
109
|
+
"start_date": start_date.strftime("%Y-%m-%d"),
|
|
110
|
+
"end_date": end_date.strftime("%Y-%m-%d"),
|
|
111
|
+
"version": sandwitches_version,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@staff_member_required
|
|
117
|
+
def admin_recipe_list(request):
|
|
118
|
+
recipes = (
|
|
119
|
+
Recipe.objects.annotate(avg_rating=Avg("ratings__score")) # ty:ignore[unresolved-attribute]
|
|
120
|
+
.prefetch_related("tags")
|
|
121
|
+
.all()
|
|
122
|
+
)
|
|
123
|
+
return render(
|
|
124
|
+
request,
|
|
125
|
+
"admin/recipe_list.html",
|
|
126
|
+
{"recipes": recipes, "version": sandwitches_version},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@staff_member_required
|
|
131
|
+
def admin_recipe_add(request):
|
|
132
|
+
if request.method == "POST":
|
|
133
|
+
form = RecipeForm(request.POST, request.FILES)
|
|
134
|
+
if form.is_valid():
|
|
135
|
+
recipe = form.save(commit=False)
|
|
136
|
+
recipe.uploaded_by = request.user
|
|
137
|
+
recipe.save()
|
|
138
|
+
form.save_m2m()
|
|
139
|
+
messages.success(request, _("Recipe added successfully."))
|
|
140
|
+
return redirect("admin_recipe_list")
|
|
141
|
+
else:
|
|
142
|
+
form = RecipeForm()
|
|
143
|
+
return render(
|
|
144
|
+
request,
|
|
145
|
+
"admin/recipe_form.html",
|
|
146
|
+
{"form": form, "title": _("Add Recipe"), "version": sandwitches_version},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@staff_member_required
|
|
151
|
+
def admin_recipe_edit(request, pk):
|
|
152
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
153
|
+
if request.method == "POST":
|
|
154
|
+
form = RecipeForm(request.POST, request.FILES, instance=recipe)
|
|
155
|
+
if form.is_valid():
|
|
156
|
+
form.save()
|
|
157
|
+
messages.success(request, _("Recipe updated successfully."))
|
|
158
|
+
return redirect("admin_recipe_list")
|
|
159
|
+
else:
|
|
160
|
+
form = RecipeForm(instance=recipe)
|
|
161
|
+
return render(
|
|
162
|
+
request,
|
|
163
|
+
"admin/recipe_form.html",
|
|
164
|
+
{
|
|
165
|
+
"form": form,
|
|
166
|
+
"recipe": recipe,
|
|
167
|
+
"title": _("Edit Recipe"),
|
|
168
|
+
"version": sandwitches_version,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@staff_member_required
|
|
174
|
+
def admin_recipe_delete(request, pk):
|
|
175
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
176
|
+
if request.method == "POST":
|
|
177
|
+
recipe.delete()
|
|
178
|
+
messages.success(request, _("Recipe deleted."))
|
|
179
|
+
return redirect("admin_recipe_list")
|
|
180
|
+
return render(
|
|
181
|
+
request,
|
|
182
|
+
"admin/confirm_delete.html",
|
|
183
|
+
{"object": recipe, "type": _("recipe"), "version": sandwitches_version},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@staff_member_required
|
|
188
|
+
def admin_recipe_rotate(request, pk):
|
|
189
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
190
|
+
direction = request.GET.get("direction", "cw")
|
|
191
|
+
if not recipe.image:
|
|
192
|
+
messages.error(request, _("No image to rotate."))
|
|
193
|
+
return redirect("admin_recipe_edit", pk=pk)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
img = Image.open(recipe.image.path)
|
|
197
|
+
if direction == "ccw":
|
|
198
|
+
# Rotate 90 degrees counter-clockwise
|
|
199
|
+
img = img.rotate(90, expand=True)
|
|
200
|
+
else:
|
|
201
|
+
# Rotate 90 degrees clockwise (default)
|
|
202
|
+
img = img.rotate(-90, expand=True)
|
|
203
|
+
img.save(recipe.image.path)
|
|
204
|
+
messages.success(request, _("Image rotated successfully."))
|
|
205
|
+
except Exception as e:
|
|
206
|
+
messages.error(request, _("Error rotating image: ") + str(e))
|
|
207
|
+
|
|
208
|
+
return redirect("admin_recipe_edit", pk=pk)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@staff_member_required
|
|
212
|
+
def admin_user_list(request):
|
|
213
|
+
users = User.objects.all()
|
|
214
|
+
return render(
|
|
215
|
+
request,
|
|
216
|
+
"admin/user_list.html",
|
|
217
|
+
{"users": users, "version": sandwitches_version},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@staff_member_required
|
|
222
|
+
def admin_user_edit(request, pk):
|
|
223
|
+
user_obj = get_object_or_404(User, pk=pk)
|
|
224
|
+
if request.method == "POST":
|
|
225
|
+
form = UserEditForm(request.POST, request.FILES, instance=user_obj)
|
|
226
|
+
if form.is_valid():
|
|
227
|
+
form.save()
|
|
228
|
+
messages.success(request, _("User updated successfully."))
|
|
229
|
+
return redirect("admin_user_list")
|
|
230
|
+
else:
|
|
231
|
+
form = UserEditForm(instance=user_obj)
|
|
232
|
+
return render(
|
|
233
|
+
request,
|
|
234
|
+
"admin/user_form.html",
|
|
235
|
+
{"form": form, "user_obj": user_obj, "version": sandwitches_version},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@staff_member_required
|
|
240
|
+
def admin_user_delete(request, pk):
|
|
241
|
+
user_obj = get_object_or_404(User, pk=pk)
|
|
242
|
+
if user_obj == request.user:
|
|
243
|
+
messages.error(request, _("You cannot delete yourself."))
|
|
244
|
+
return redirect("admin_user_list")
|
|
245
|
+
if request.method == "POST":
|
|
246
|
+
user_obj.delete()
|
|
247
|
+
messages.success(request, _("User deleted."))
|
|
248
|
+
return redirect("admin_user_list")
|
|
249
|
+
return render(
|
|
250
|
+
request,
|
|
251
|
+
"admin/confirm_delete.html",
|
|
252
|
+
{"object": user_obj, "type": _("user"), "version": sandwitches_version},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@staff_member_required
|
|
257
|
+
def admin_tag_list(request):
|
|
258
|
+
tags = Tag.objects.all() # ty:ignore[unresolved-attribute]
|
|
259
|
+
return render(
|
|
260
|
+
request,
|
|
261
|
+
"admin/tag_list.html",
|
|
262
|
+
{"tags": tags, "version": sandwitches_version},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@staff_member_required
|
|
267
|
+
def admin_tag_add(request):
|
|
268
|
+
if request.method == "POST":
|
|
269
|
+
form = TagForm(request.POST)
|
|
270
|
+
if form.is_valid():
|
|
271
|
+
form.save()
|
|
272
|
+
messages.success(request, _("Tag added successfully."))
|
|
273
|
+
return redirect("admin_tag_list")
|
|
274
|
+
else:
|
|
275
|
+
form = TagForm()
|
|
276
|
+
return render(
|
|
277
|
+
request,
|
|
278
|
+
"admin/tag_form.html",
|
|
279
|
+
{"form": form, "title": _("Add Tag"), "version": sandwitches_version},
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@staff_member_required
|
|
284
|
+
def admin_tag_edit(request, pk):
|
|
285
|
+
tag = get_object_or_404(Tag, pk=pk)
|
|
286
|
+
if request.method == "POST":
|
|
287
|
+
form = TagForm(request.POST, instance=tag)
|
|
288
|
+
if form.is_valid():
|
|
289
|
+
form.save()
|
|
290
|
+
messages.success(request, _("Tag updated successfully."))
|
|
291
|
+
return redirect("admin_tag_list")
|
|
292
|
+
else:
|
|
293
|
+
form = TagForm(instance=tag)
|
|
294
|
+
return render(
|
|
295
|
+
request,
|
|
296
|
+
"admin/tag_form.html",
|
|
297
|
+
{
|
|
298
|
+
"form": form,
|
|
299
|
+
"tag": tag,
|
|
300
|
+
"title": _("Edit Tag"),
|
|
301
|
+
"version": sandwitches_version,
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@staff_member_required
|
|
307
|
+
def admin_tag_delete(request, pk):
|
|
308
|
+
tag = get_object_or_404(Tag, pk=pk)
|
|
309
|
+
if request.method == "POST":
|
|
310
|
+
tag.delete()
|
|
311
|
+
messages.success(request, _("Tag deleted."))
|
|
312
|
+
return redirect("admin_tag_list")
|
|
313
|
+
return render(
|
|
314
|
+
request,
|
|
315
|
+
"admin/confirm_delete.html",
|
|
316
|
+
{"object": tag, "type": _("tag"), "version": sandwitches_version},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@staff_member_required
|
|
321
|
+
def admin_task_list(request):
|
|
322
|
+
tasks = DBTaskResult.objects.all().order_by("-enqueued_at")[:50]
|
|
323
|
+
return render(
|
|
324
|
+
request,
|
|
325
|
+
"admin/task_list.html",
|
|
326
|
+
{"tasks": tasks, "version": sandwitches_version},
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@staff_member_required
|
|
331
|
+
def admin_task_detail(request, pk):
|
|
332
|
+
task = get_object_or_404(DBTaskResult, pk=pk)
|
|
333
|
+
return render(
|
|
334
|
+
request,
|
|
335
|
+
"admin/task_detail.html",
|
|
336
|
+
{"task": task, "version": sandwitches_version},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@staff_member_required
|
|
341
|
+
def admin_rating_list(request):
|
|
342
|
+
ratings = (
|
|
343
|
+
Rating.objects.select_related("recipe", "user") # ty:ignore[unresolved-attribute]
|
|
344
|
+
.all()
|
|
345
|
+
.order_by("-updated_at")
|
|
346
|
+
)
|
|
347
|
+
return render(
|
|
348
|
+
request,
|
|
349
|
+
"admin/rating_list.html",
|
|
350
|
+
{"ratings": ratings, "version": sandwitches_version},
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@staff_member_required
|
|
355
|
+
def admin_rating_delete(request, pk):
|
|
356
|
+
rating = get_object_or_404(Rating, pk=pk)
|
|
357
|
+
if request.method == "POST":
|
|
358
|
+
rating.delete()
|
|
359
|
+
messages.success(request, _("Rating deleted."))
|
|
360
|
+
return redirect("admin_rating_list")
|
|
361
|
+
return render(
|
|
362
|
+
request,
|
|
363
|
+
"admin/confirm_delete.html",
|
|
364
|
+
{"object": rating, "type": _("rating"), "version": sandwitches_version},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def recipe_detail(request, slug):
|
|
369
|
+
recipe = get_object_or_404(Recipe, slug=slug)
|
|
370
|
+
avg = recipe.average_rating()
|
|
371
|
+
count = recipe.rating_count()
|
|
372
|
+
user_rating = None
|
|
373
|
+
rating_form = None
|
|
374
|
+
if request.user.is_authenticated:
|
|
375
|
+
try:
|
|
376
|
+
user_rating = Rating.objects.get(recipe=recipe, user=request.user) # ty:ignore[unresolved-attribute]
|
|
377
|
+
except Rating.DoesNotExist: # ty:ignore[unresolved-attribute]
|
|
378
|
+
user_rating = None
|
|
379
|
+
# show form prefilled when possible
|
|
380
|
+
initial = (
|
|
381
|
+
{"score": str(user_rating.score), "comment": user_rating.comment}
|
|
382
|
+
if user_rating
|
|
383
|
+
else None
|
|
384
|
+
)
|
|
385
|
+
rating_form = RatingForm(initial=initial)
|
|
386
|
+
return render(
|
|
387
|
+
request,
|
|
388
|
+
"detail.html",
|
|
389
|
+
{
|
|
390
|
+
"recipe": recipe,
|
|
391
|
+
"avg_rating": avg,
|
|
392
|
+
"rating_count": count,
|
|
393
|
+
"user_rating": user_rating,
|
|
394
|
+
"rating_form": rating_form,
|
|
395
|
+
"version": sandwitches_version,
|
|
396
|
+
"all_ratings": recipe.ratings.select_related("user").order_by(
|
|
397
|
+
"-created_at"
|
|
398
|
+
), # Add all ratings for display
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@login_required
|
|
404
|
+
def recipe_rate(request, pk):
|
|
405
|
+
"""
|
|
406
|
+
Create or update a rating for the given recipe by the logged-in user.
|
|
407
|
+
"""
|
|
408
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
409
|
+
if request.method != "POST":
|
|
410
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
411
|
+
|
|
412
|
+
form = RatingForm(request.POST)
|
|
413
|
+
if form.is_valid():
|
|
414
|
+
score = form.cleaned_data["score"]
|
|
415
|
+
comment = form.cleaned_data["comment"]
|
|
416
|
+
Rating.objects.update_or_create( # ty:ignore[unresolved-attribute]
|
|
417
|
+
recipe=recipe,
|
|
418
|
+
user=request.user,
|
|
419
|
+
defaults={"score": score, "comment": comment},
|
|
420
|
+
)
|
|
421
|
+
messages.success(request, _("Your rating has been saved."))
|
|
422
|
+
else:
|
|
423
|
+
messages.error(request, _("Could not save rating."))
|
|
424
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@login_required
|
|
428
|
+
def toggle_favorite(request, pk):
|
|
429
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
430
|
+
if recipe in request.user.favorites.all():
|
|
431
|
+
request.user.favorites.remove(recipe)
|
|
432
|
+
messages.success(request, _("Recipe removed from favorites."))
|
|
433
|
+
else:
|
|
434
|
+
request.user.favorites.add(recipe)
|
|
435
|
+
messages.success(request, _("Recipe added to favorites."))
|
|
436
|
+
|
|
437
|
+
# Redirect to the page where the request came from, or default to recipe detail
|
|
438
|
+
referer = request.META.get("HTTP_REFERER")
|
|
439
|
+
if referer:
|
|
440
|
+
return redirect(referer)
|
|
441
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@login_required
|
|
445
|
+
def favorites(request):
|
|
446
|
+
recipes = request.user.favorites.all().prefetch_related("favorited_by")
|
|
447
|
+
|
|
448
|
+
# Filtering
|
|
449
|
+
q = request.GET.get("q")
|
|
450
|
+
if q:
|
|
451
|
+
recipes = recipes.filter(
|
|
452
|
+
Q(title__icontains=q) | Q(tags__name__icontains=q)
|
|
453
|
+
).distinct()
|
|
454
|
+
|
|
455
|
+
date_start = request.GET.get("date_start")
|
|
456
|
+
if date_start:
|
|
457
|
+
recipes = recipes.filter(created_at__gte=date_start)
|
|
458
|
+
|
|
459
|
+
date_end = request.GET.get("date_end")
|
|
460
|
+
if date_end:
|
|
461
|
+
recipes = recipes.filter(created_at__lte=date_end)
|
|
462
|
+
|
|
463
|
+
uploader = request.GET.get("uploader")
|
|
464
|
+
if uploader:
|
|
465
|
+
recipes = recipes.filter(uploaded_by__username=uploader)
|
|
466
|
+
|
|
467
|
+
tag = request.GET.get("tag")
|
|
468
|
+
if tag:
|
|
469
|
+
recipes = recipes.filter(tags__name=tag)
|
|
470
|
+
|
|
471
|
+
# Sorting
|
|
472
|
+
sort = request.GET.get("sort", "date_desc")
|
|
473
|
+
if sort == "date_asc":
|
|
474
|
+
recipes = recipes.order_by("created_at")
|
|
475
|
+
elif sort == "rating":
|
|
476
|
+
recipes = recipes.annotate(avg_rating=Avg("ratings__score")).order_by(
|
|
477
|
+
"-avg_rating", "-created_at"
|
|
478
|
+
)
|
|
479
|
+
elif sort == "user":
|
|
480
|
+
recipes = recipes.order_by("uploaded_by__username", "-created_at")
|
|
481
|
+
else: # date_desc or default
|
|
482
|
+
recipes = recipes.order_by("-created_at")
|
|
483
|
+
|
|
484
|
+
if request.headers.get("HX-Request"):
|
|
485
|
+
return render(
|
|
486
|
+
request,
|
|
487
|
+
"partials/recipe_list.html",
|
|
488
|
+
{"recipes": recipes, "user": request.user},
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Context for filters - only show options relevant to favorited recipes
|
|
492
|
+
uploaders = User.objects.filter(recipes__in=request.user.favorites.all()).distinct()
|
|
493
|
+
tags = Tag.objects.filter(recipes__in=request.user.favorites.all()).distinct() # ty:ignore[unresolved-attribute]
|
|
494
|
+
|
|
495
|
+
return render(
|
|
496
|
+
request,
|
|
497
|
+
"favorites.html",
|
|
498
|
+
{
|
|
499
|
+
"recipes": recipes,
|
|
500
|
+
"version": sandwitches_version,
|
|
501
|
+
"uploaders": uploaders,
|
|
502
|
+
"tags": tags,
|
|
503
|
+
"selected_tags": request.GET.getlist("tag"), # Add selected_tags
|
|
504
|
+
"user": request.user,
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def index(request):
|
|
510
|
+
if not User.objects.filter(is_superuser=True).exists():
|
|
511
|
+
return redirect("setup")
|
|
512
|
+
recipes = (
|
|
513
|
+
Recipe.objects.all().prefetch_related("favorited_by") # ty:ignore[unresolved-attribute]
|
|
514
|
+
) # Start with all, order later
|
|
515
|
+
|
|
516
|
+
# Filtering
|
|
517
|
+
q = request.GET.get("q")
|
|
518
|
+
if q:
|
|
519
|
+
recipes = recipes.filter(
|
|
520
|
+
Q(title__icontains=q) | Q(tags__name__icontains=q)
|
|
521
|
+
).distinct()
|
|
522
|
+
|
|
523
|
+
date_start = request.GET.get("date_start")
|
|
524
|
+
if date_start:
|
|
525
|
+
recipes = recipes.filter(created_at__gte=date_start)
|
|
526
|
+
|
|
527
|
+
date_end = request.GET.get("date_end")
|
|
528
|
+
if date_end:
|
|
529
|
+
recipes = recipes.filter(created_at__lte=date_end)
|
|
530
|
+
|
|
531
|
+
uploader = request.GET.get("uploader")
|
|
532
|
+
if uploader:
|
|
533
|
+
recipes = recipes.filter(uploaded_by__username=uploader)
|
|
534
|
+
|
|
535
|
+
tags = request.GET.getlist("tag")
|
|
536
|
+
if tags:
|
|
537
|
+
recipes = recipes.filter(tags__name__in=tags)
|
|
538
|
+
|
|
539
|
+
if request.user.is_authenticated and request.GET.get("favorites") == "on":
|
|
540
|
+
recipes = recipes.filter(pk__in=request.user.favorites.values("pk"))
|
|
541
|
+
|
|
542
|
+
# Sorting
|
|
543
|
+
sort = request.GET.get("sort", "date_desc")
|
|
544
|
+
if sort == "date_asc":
|
|
545
|
+
recipes = recipes.order_by("created_at")
|
|
546
|
+
elif sort == "rating":
|
|
547
|
+
recipes = recipes.annotate(avg_rating=Avg("ratings__score")).order_by(
|
|
548
|
+
"-avg_rating", "-created_at"
|
|
549
|
+
)
|
|
550
|
+
elif sort == "user":
|
|
551
|
+
recipes = recipes.order_by("uploaded_by__username", "-created_at")
|
|
552
|
+
else: # date_desc or default
|
|
553
|
+
recipes = recipes.order_by("-created_at")
|
|
554
|
+
|
|
555
|
+
if request.headers.get("HX-Request"):
|
|
556
|
+
return render(
|
|
557
|
+
request,
|
|
558
|
+
"partials/recipe_list.html",
|
|
559
|
+
{"recipes": recipes, "user": request.user},
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Context for filters
|
|
563
|
+
uploaders = User.objects.filter(recipes__isnull=False).distinct()
|
|
564
|
+
tags = Tag.objects.all() # ty:ignore[unresolved-attribute]
|
|
565
|
+
|
|
566
|
+
highlighted_recipes = Recipe.objects.filter(is_highlighted=True) # ty:ignore[unresolved-attribute]
|
|
567
|
+
|
|
568
|
+
return render(
|
|
569
|
+
request,
|
|
570
|
+
"index.html",
|
|
571
|
+
{
|
|
572
|
+
"recipes": recipes,
|
|
573
|
+
"version": sandwitches_version,
|
|
574
|
+
"uploaders": uploaders,
|
|
575
|
+
"tags": tags,
|
|
576
|
+
"selected_tags": request.GET.getlist("tag"),
|
|
577
|
+
"user": request.user, # Pass user to template
|
|
578
|
+
"highlighted_recipes": highlighted_recipes,
|
|
579
|
+
},
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def setup(request):
|
|
584
|
+
"""
|
|
585
|
+
First-time setup page: create initial superuser if none exists.
|
|
586
|
+
Visible only while there are no superusers in the DB.
|
|
587
|
+
"""
|
|
588
|
+
# do not allow access if a superuser already exists
|
|
589
|
+
if User.objects.filter(is_superuser=True).exists():
|
|
590
|
+
return redirect("index")
|
|
591
|
+
|
|
592
|
+
if request.method == "POST":
|
|
593
|
+
form = AdminSetupForm(request.POST)
|
|
594
|
+
if form.is_valid():
|
|
595
|
+
user = form.save()
|
|
596
|
+
user.backend = "django.contrib.auth.backends.ModelBackend"
|
|
597
|
+
login(request, user)
|
|
598
|
+
messages.success(request, _("Admin account created and signed in."))
|
|
599
|
+
return redirect(reverse("admin:index"))
|
|
600
|
+
else:
|
|
601
|
+
form = AdminSetupForm()
|
|
602
|
+
|
|
603
|
+
return render(request, "setup.html", {"form": form, "version": sandwitches_version})
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def signup(request):
|
|
607
|
+
"""
|
|
608
|
+
User signup page: create new regular user accounts.
|
|
609
|
+
"""
|
|
610
|
+
if request.method == "POST":
|
|
611
|
+
form = UserSignupForm(request.POST, request.FILES)
|
|
612
|
+
if form.is_valid():
|
|
613
|
+
user = form.save()
|
|
614
|
+
# log in the newly created user
|
|
615
|
+
user.backend = "django.contrib.auth.backends.ModelBackend"
|
|
616
|
+
login(request, user)
|
|
617
|
+
messages.success(request, _("Account created and signed in."))
|
|
618
|
+
return redirect("index")
|
|
619
|
+
else:
|
|
620
|
+
form = UserSignupForm()
|
|
621
|
+
|
|
622
|
+
return render(
|
|
623
|
+
request, "signup.html", {"form": form, "version": sandwitches_version}
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def media(request, file_path=None):
|
|
628
|
+
media_root = getattr(settings, "MEDIA_ROOT", None)
|
|
629
|
+
if not media_root:
|
|
630
|
+
return HttpResponseBadRequest("Invalid Media Root Configuration")
|
|
631
|
+
if not file_path:
|
|
632
|
+
return HttpResponseBadRequest("Invalid File Path")
|
|
633
|
+
|
|
634
|
+
base_path = Path(media_root).resolve()
|
|
635
|
+
full_path = base_path.joinpath(file_path).resolve()
|
|
636
|
+
if base_path not in full_path.parents:
|
|
637
|
+
return HttpResponseBadRequest("Access Denied")
|
|
638
|
+
|
|
639
|
+
if not full_path.exists() or not full_path.is_file():
|
|
640
|
+
raise Http404("File not found")
|
|
641
|
+
|
|
642
|
+
content_type, encoding = mimetypes.guess_type(full_path)
|
|
643
|
+
if not content_type or not content_type.startswith("image/"):
|
|
644
|
+
return HttpResponseBadRequest("Access Denied: Only image files are allowed.")
|
|
645
|
+
|
|
646
|
+
response = FileResponse(open(full_path, "rb"), as_attachment=True)
|
|
647
|
+
return response
|
sandwitches/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WSGI config for sandwitches project.
|
|
3
|
+
|
|
4
|
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
5
|
+
|
|
6
|
+
For more information on this file, see
|
|
7
|
+
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from django.core.wsgi import get_wsgi_application
|
|
13
|
+
|
|
14
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandwitches.settings")
|
|
15
|
+
|
|
16
|
+
application = get_wsgi_application()
|