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.
Files changed (65) hide show
  1. sandwitches/__init__.py +6 -0
  2. sandwitches/admin.py +69 -0
  3. sandwitches/api.py +207 -0
  4. sandwitches/asgi.py +16 -0
  5. sandwitches/feeds.py +23 -0
  6. sandwitches/forms.py +196 -0
  7. sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  8. sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
  9. sandwitches/migrations/0001_initial.py +328 -0
  10. sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
  11. sandwitches/migrations/0003_setting.py +35 -0
  12. sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
  13. sandwitches/migrations/0005_rating_comment.py +17 -0
  14. sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +22 -0
  15. sandwitches/migrations/__init__.py +0 -0
  16. sandwitches/models.py +218 -0
  17. sandwitches/settings.py +220 -0
  18. sandwitches/storage.py +114 -0
  19. sandwitches/tasks.py +115 -0
  20. sandwitches/templates/admin/admin_base.html +118 -0
  21. sandwitches/templates/admin/confirm_delete.html +23 -0
  22. sandwitches/templates/admin/dashboard.html +262 -0
  23. sandwitches/templates/admin/rating_list.html +38 -0
  24. sandwitches/templates/admin/recipe_form.html +184 -0
  25. sandwitches/templates/admin/recipe_list.html +64 -0
  26. sandwitches/templates/admin/tag_form.html +30 -0
  27. sandwitches/templates/admin/tag_list.html +37 -0
  28. sandwitches/templates/admin/task_detail.html +91 -0
  29. sandwitches/templates/admin/task_list.html +41 -0
  30. sandwitches/templates/admin/user_form.html +37 -0
  31. sandwitches/templates/admin/user_list.html +60 -0
  32. sandwitches/templates/base.html +94 -0
  33. sandwitches/templates/base_beer.html +57 -0
  34. sandwitches/templates/components/carousel_scripts.html +59 -0
  35. sandwitches/templates/components/favorites_search_form.html +85 -0
  36. sandwitches/templates/components/footer.html +14 -0
  37. sandwitches/templates/components/ingredients_scripts.html +50 -0
  38. sandwitches/templates/components/ingredients_section.html +11 -0
  39. sandwitches/templates/components/instructions_section.html +9 -0
  40. sandwitches/templates/components/language_dialog.html +26 -0
  41. sandwitches/templates/components/navbar.html +27 -0
  42. sandwitches/templates/components/rating_section.html +66 -0
  43. sandwitches/templates/components/recipe_header.html +32 -0
  44. sandwitches/templates/components/search_form.html +106 -0
  45. sandwitches/templates/components/search_scripts.html +98 -0
  46. sandwitches/templates/components/side_menu.html +35 -0
  47. sandwitches/templates/components/user_menu.html +10 -0
  48. sandwitches/templates/detail.html +178 -0
  49. sandwitches/templates/favorites.html +42 -0
  50. sandwitches/templates/index.html +76 -0
  51. sandwitches/templates/login.html +57 -0
  52. sandwitches/templates/partials/recipe_list.html +87 -0
  53. sandwitches/templates/recipe_form.html +119 -0
  54. sandwitches/templates/setup.html +105 -0
  55. sandwitches/templates/signup.html +133 -0
  56. sandwitches/templatetags/__init__.py +0 -0
  57. sandwitches/templatetags/custom_filters.py +15 -0
  58. sandwitches/templatetags/markdown_extras.py +17 -0
  59. sandwitches/urls.py +109 -0
  60. sandwitches/utils.py +222 -0
  61. sandwitches/views.py +647 -0
  62. sandwitches/wsgi.py +16 -0
  63. sandwitches-2.2.0.dist-info/METADATA +104 -0
  64. sandwitches-2.2.0.dist-info/RECORD +65 -0
  65. 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()