sandwitches 1.1.0__py3-none-any.whl → 1.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/admin.py +22 -2
- sandwitches/api.py +27 -10
- sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches/locale/nl/LC_MESSAGES/django.po +139 -0
- sandwitches/migrations/0004_add_uploaded_by.py +25 -0
- sandwitches/migrations/0005_historicalrecipe_uploaded_by.py +27 -0
- sandwitches/migrations/0006_profile.py +48 -0
- sandwitches/models.py +90 -20
- sandwitches/settings.py +89 -22
- sandwitches/storage.py +57 -4
- sandwitches/tasks.py +91 -8
- sandwitches/templates/base.html +2 -1
- sandwitches/templates/base_pico.html +20 -6
- sandwitches/templates/detail.html +36 -17
- sandwitches/templates/form.html +5 -4
- sandwitches/templates/index.html +10 -6
- sandwitches/templates/setup.html +9 -7
- sandwitches/templates/signup.html +11 -11
- sandwitches/urls.py +12 -12
- sandwitches/views.py +37 -10
- {sandwitches-1.1.0.dist-info → sandwitches-1.2.0.dist-info}/METADATA +5 -2
- sandwitches-1.2.0.dist-info/RECORD +33 -0
- sandwitches-1.1.0.dist-info/RECORD +0 -28
- {sandwitches-1.1.0.dist-info → sandwitches-1.2.0.dist-info}/WHEEL +0 -0
sandwitches/admin.py
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
|
-
from .models import Recipe, Tag
|
|
2
|
+
from .models import Recipe, Tag, Rating, Profile
|
|
3
|
+
from django.utils.html import format_html
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(Recipe)
|
|
7
|
+
class RecipeAdmin(admin.ModelAdmin):
|
|
8
|
+
list_display = ("title", "uploaded_by", "created_at", "show_url")
|
|
9
|
+
readonly_fields = ("created_at", "updated_at")
|
|
10
|
+
|
|
11
|
+
def save_model(self, request, obj, form, change):
|
|
12
|
+
# set uploaded_by automatically when creating in admin
|
|
13
|
+
if not change and not obj.uploaded_by:
|
|
14
|
+
obj.uploaded_by = request.user
|
|
15
|
+
super().save_model(request, obj, form, change)
|
|
16
|
+
|
|
17
|
+
def show_url(self, obj):
|
|
18
|
+
url = obj.get_absolute_url()
|
|
19
|
+
return format_html("<a href='{url}'>{url}</a>", url=url)
|
|
20
|
+
|
|
21
|
+
show_url.short_description = "Recipe Link" # ty:ignore[unresolved-attribute]
|
|
3
22
|
|
|
4
23
|
|
|
5
|
-
admin.site.register(Recipe)
|
|
6
24
|
admin.site.register(Tag)
|
|
25
|
+
admin.site.register(Rating)
|
|
26
|
+
admin.site.register(Profile)
|
sandwitches/api.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ninja import NinjaAPI
|
|
2
|
-
from .models import Recipe
|
|
2
|
+
from .models import Recipe, Tag
|
|
3
3
|
|
|
4
4
|
from ninja import ModelSchema
|
|
5
5
|
from ninja import Schema
|
|
@@ -8,8 +8,11 @@ from django.shortcuts import get_object_or_404
|
|
|
8
8
|
from datetime import date
|
|
9
9
|
import random
|
|
10
10
|
|
|
11
|
+
from ninja.security import django_auth
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
from __init__ import __version__
|
|
14
|
+
|
|
15
|
+
api = NinjaAPI(version=__version__)
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class RecipeSchema(ModelSchema):
|
|
@@ -18,6 +21,12 @@ class RecipeSchema(ModelSchema):
|
|
|
18
21
|
fields = "__all__"
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
class TagSchema(ModelSchema):
|
|
25
|
+
class Meta:
|
|
26
|
+
model = Tag
|
|
27
|
+
fields = "__all__"
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
class UserSchema(ModelSchema):
|
|
22
31
|
class Meta:
|
|
23
32
|
model = User
|
|
@@ -35,17 +44,14 @@ def me(request):
|
|
|
35
44
|
return request.user
|
|
36
45
|
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# recipe = Recipe.objects.create(**payload.dict())
|
|
42
|
-
# return recipe
|
|
47
|
+
@api.get("v1/users", auth=django_auth, response=list[UserSchema])
|
|
48
|
+
def users(request):
|
|
49
|
+
return User.objects.all()
|
|
43
50
|
|
|
44
51
|
|
|
45
52
|
@api.get("v1/recipes", response=list[RecipeSchema])
|
|
46
53
|
def get_recipes(request):
|
|
47
|
-
|
|
48
|
-
return recipes
|
|
54
|
+
return Recipe.objects.all() # ty:ignore[unresolved-attribute]
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
@api.get("v1/recipes/{recipe_id}", response=RecipeSchema)
|
|
@@ -56,10 +62,21 @@ def get_recipe(request, recipe_id: int):
|
|
|
56
62
|
|
|
57
63
|
@api.get("v1/recipe-of-the-day", response=RecipeSchema)
|
|
58
64
|
def get_recipe_of_the_day(request):
|
|
59
|
-
recipes = list(Recipe.objects.all())
|
|
65
|
+
recipes = list(Recipe.objects.all()) # ty:ignore[unresolved-attribute]
|
|
60
66
|
if not recipes:
|
|
61
67
|
return None
|
|
62
68
|
today = date.today()
|
|
63
69
|
random.seed(today.toordinal())
|
|
64
70
|
recipe = random.choice(recipes)
|
|
65
71
|
return recipe
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@api.get("v1/tags", response=list[TagSchema])
|
|
75
|
+
def get_tags(request):
|
|
76
|
+
return Tag.objects.all() # ty:ignore[unresolved-attribute]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@api.get("v1/tags/{tag_id}", response=TagSchema)
|
|
80
|
+
def get_tag(request, tag_id: int):
|
|
81
|
+
tag = get_object_or_404(Tag, id=tag_id)
|
|
82
|
+
return tag
|
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Dutch translations for Sandwitches project
|
|
2
|
+
msgid ""
|
|
3
|
+
msgstr ""
|
|
4
|
+
"Project-Id-Version: sandwitches 1.0\n"
|
|
5
|
+
"POT-Creation-Date: 2025-01-01 00:00+0000\n"
|
|
6
|
+
"PO-Revision-Date: 2025-01-01 00:00+0000\n"
|
|
7
|
+
"Language: nl\n"
|
|
8
|
+
"MIME-Version: 1.0\n"
|
|
9
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
10
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
11
|
+
|
|
12
|
+
msgid "Sign up"
|
|
13
|
+
msgstr "Aanmelden"
|
|
14
|
+
|
|
15
|
+
msgid "Sign Up"
|
|
16
|
+
msgstr "Aanmelden"
|
|
17
|
+
|
|
18
|
+
msgid "Cancel"
|
|
19
|
+
msgstr "Annuleren"
|
|
20
|
+
|
|
21
|
+
msgid "Username"
|
|
22
|
+
msgstr "Gebruikersnaam"
|
|
23
|
+
|
|
24
|
+
msgid "Email (optional)"
|
|
25
|
+
msgstr "E-mail (optioneel)"
|
|
26
|
+
|
|
27
|
+
msgid "First name"
|
|
28
|
+
msgstr "Voornaam"
|
|
29
|
+
|
|
30
|
+
msgid "Last name"
|
|
31
|
+
msgstr "Achternaam"
|
|
32
|
+
|
|
33
|
+
msgid "Password"
|
|
34
|
+
msgstr "Wachtwoord"
|
|
35
|
+
|
|
36
|
+
msgid "Confirm password"
|
|
37
|
+
msgstr "Bevestig wachtwoord"
|
|
38
|
+
|
|
39
|
+
msgid "Back to all"
|
|
40
|
+
msgstr "Terug naar alles"
|
|
41
|
+
|
|
42
|
+
msgid "Description"
|
|
43
|
+
msgstr "Beschrijving"
|
|
44
|
+
|
|
45
|
+
msgid "Ingredients"
|
|
46
|
+
msgstr "Ingrediënten"
|
|
47
|
+
|
|
48
|
+
msgid "Instructions"
|
|
49
|
+
msgstr "Instructies"
|
|
50
|
+
|
|
51
|
+
msgid "No description yet."
|
|
52
|
+
msgstr "Nog geen beschrijving."
|
|
53
|
+
|
|
54
|
+
msgid "No ingredients listed."
|
|
55
|
+
msgstr "Geen ingrediënten vermeld."
|
|
56
|
+
|
|
57
|
+
msgid "No instructions yet."
|
|
58
|
+
msgstr "Nog geen instructies."
|
|
59
|
+
|
|
60
|
+
msgid "Rating"
|
|
61
|
+
msgstr "Beoordeling"
|
|
62
|
+
|
|
63
|
+
msgid "Average:"
|
|
64
|
+
msgstr "Gemiddelde:"
|
|
65
|
+
|
|
66
|
+
msgid "No ratings yet."
|
|
67
|
+
msgstr "Nog geen beoordelingen."
|
|
68
|
+
|
|
69
|
+
msgid "Your rating:"
|
|
70
|
+
msgstr "Jouw beoordeling:"
|
|
71
|
+
|
|
72
|
+
msgid "What would you rate this sandwich?"
|
|
73
|
+
msgstr "Wat zou je dit broodje geven?"
|
|
74
|
+
|
|
75
|
+
msgid "Update"
|
|
76
|
+
msgstr "Bijwerken"
|
|
77
|
+
|
|
78
|
+
msgid "Rate"
|
|
79
|
+
msgstr "Beoordeel"
|
|
80
|
+
|
|
81
|
+
msgid "Login"
|
|
82
|
+
msgstr "Inloggen"
|
|
83
|
+
|
|
84
|
+
msgid "to rate this recipe."
|
|
85
|
+
msgstr "om dit recept te beoordelen."
|
|
86
|
+
|
|
87
|
+
msgid "Docs"
|
|
88
|
+
msgstr "Docs"
|
|
89
|
+
|
|
90
|
+
msgid "Admin"
|
|
91
|
+
msgstr "Beheer"
|
|
92
|
+
|
|
93
|
+
msgid "Logout"
|
|
94
|
+
msgstr "Uitloggen"
|
|
95
|
+
|
|
96
|
+
msgid "Initial setup — Create admin"
|
|
97
|
+
msgstr "Eerste installatie — Beheerder aanmaken"
|
|
98
|
+
|
|
99
|
+
msgid "Create initial administrator"
|
|
100
|
+
msgstr "Maak de eerste beheerder"
|
|
101
|
+
|
|
102
|
+
msgid "This page is only available when there are no admin users in the database."
|
|
103
|
+
msgstr "Deze pagina is alleen beschikbaar als er nog geen beheerders in de database zijn."
|
|
104
|
+
|
|
105
|
+
msgid "After creating the account you will be logged in and redirected to the admin."
|
|
106
|
+
msgstr "Na aanmaken wordt u ingelogd en doorgestuurd naar het beheerpaneel."
|
|
107
|
+
|
|
108
|
+
msgid "Create admin"
|
|
109
|
+
msgstr "Maak beheerder"
|
|
110
|
+
|
|
111
|
+
msgid "Account created and signed in."
|
|
112
|
+
msgstr "Account aangemaakt en ingelogd."
|
|
113
|
+
|
|
114
|
+
msgid "Admin account created and signed in."
|
|
115
|
+
msgstr "Beheerderaccount aangemaakt en ingelogd."
|
|
116
|
+
|
|
117
|
+
msgid "Your rating has been saved."
|
|
118
|
+
msgstr "Je beoordeling is opgeslagen."
|
|
119
|
+
|
|
120
|
+
msgid "Could not save rating."
|
|
121
|
+
msgstr "Kon de beoordeling niet opslaan."
|
|
122
|
+
|
|
123
|
+
msgid "Uploaded by"
|
|
124
|
+
msgstr "Geüpload door"
|
|
125
|
+
|
|
126
|
+
msgid "Search by title or tag"
|
|
127
|
+
msgstr "Zoek op titel of tag"
|
|
128
|
+
|
|
129
|
+
msgid "Search"
|
|
130
|
+
msgstr "Zoeken"
|
|
131
|
+
|
|
132
|
+
msgid "No sandwitches yet, please stay tuned."
|
|
133
|
+
msgstr "Nog geen broodjes, kom later terug."
|
|
134
|
+
|
|
135
|
+
msgid "Edit"
|
|
136
|
+
msgstr "Bewerk"
|
|
137
|
+
|
|
138
|
+
msgid "Sandwitches: sandwiches so good, they haunt you!"
|
|
139
|
+
msgstr "Sandwitches: broodjes zo lekker, dat ze je achtervolgen!"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by ChatGPT — add uploaded_by to Recipe
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("sandwitches", "0003_rating"),
|
|
10
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="recipe",
|
|
16
|
+
name="uploaded_by",
|
|
17
|
+
field=models.ForeignKey(
|
|
18
|
+
blank=True,
|
|
19
|
+
null=True,
|
|
20
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
21
|
+
related_name="recipes",
|
|
22
|
+
to=settings.AUTH_USER_MODEL,
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-28 18:36
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("sandwitches", "0004_add_uploaded_by"),
|
|
11
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AddField(
|
|
16
|
+
model_name="historicalrecipe",
|
|
17
|
+
name="uploaded_by",
|
|
18
|
+
field=models.ForeignKey(
|
|
19
|
+
blank=True,
|
|
20
|
+
db_constraint=False,
|
|
21
|
+
null=True,
|
|
22
|
+
on_delete=django.db.models.deletion.DO_NOTHING,
|
|
23
|
+
related_name="+",
|
|
24
|
+
to=settings.AUTH_USER_MODEL,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-29 17:38
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("sandwitches", "0005_historicalrecipe_uploaded_by"),
|
|
11
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="Profile",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.BigAutoField(
|
|
21
|
+
auto_created=True,
|
|
22
|
+
primary_key=True,
|
|
23
|
+
serialize=False,
|
|
24
|
+
verbose_name="ID",
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"avatar",
|
|
29
|
+
models.ImageField(blank=True, null=True, upload_to="avatars"),
|
|
30
|
+
),
|
|
31
|
+
("bio", models.TextField(blank=True)),
|
|
32
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
33
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
34
|
+
(
|
|
35
|
+
"user",
|
|
36
|
+
models.OneToOneField(
|
|
37
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
38
|
+
related_name="profile",
|
|
39
|
+
to=settings.AUTH_USER_MODEL,
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
options={
|
|
44
|
+
"verbose_name": "Profile",
|
|
45
|
+
"verbose_name_plural": "Profiles",
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
]
|
sandwitches/models.py
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
from django.db import models
|
|
2
|
-
from django.urls import reverse
|
|
3
2
|
from django.utils.text import slugify
|
|
4
3
|
from .storage import HashedFilenameStorage
|
|
5
4
|
from simple_history.models import HistoricalRecords
|
|
6
5
|
from django.contrib.auth import get_user_model
|
|
7
6
|
from django.db.models import Avg
|
|
7
|
+
from .tasks import email_users
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
import logging
|
|
10
|
+
from django.urls import reverse
|
|
11
|
+
|
|
12
|
+
from imagekit.models import ImageSpecField
|
|
13
|
+
from imagekit.processors import ResizeToFill
|
|
8
14
|
|
|
9
15
|
hashed_storage = HashedFilenameStorage()
|
|
10
16
|
|
|
11
17
|
User = get_user_model()
|
|
12
18
|
|
|
13
19
|
|
|
20
|
+
class Profile(models.Model):
|
|
21
|
+
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
|
22
|
+
avatar = models.ImageField(upload_to="avatars", blank=True, null=True)
|
|
23
|
+
avatar_thumbnail = ImageSpecField(
|
|
24
|
+
source="avatar",
|
|
25
|
+
processors=[ResizeToFill(100, 50)],
|
|
26
|
+
format="JPEG",
|
|
27
|
+
options={"quality": 60},
|
|
28
|
+
)
|
|
29
|
+
bio = models.TextField(blank=True)
|
|
30
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
31
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
verbose_name = "Profile"
|
|
35
|
+
verbose_name_plural = "Profiles"
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return f"{self.user.username}'s Profile" # ty:ignore[possibly-missing-attribute]
|
|
39
|
+
|
|
40
|
+
|
|
14
41
|
class Tag(models.Model):
|
|
15
42
|
name = models.CharField(max_length=50, unique=True)
|
|
16
43
|
slug = models.SlugField(max_length=60, unique=True, blank=True)
|
|
@@ -25,7 +52,7 @@ class Tag(models.Model):
|
|
|
25
52
|
base = slugify(self.name)[:55]
|
|
26
53
|
slug = base
|
|
27
54
|
n = 1
|
|
28
|
-
while Tag.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
|
55
|
+
while Tag.objects.filter(slug=slug).exclude(pk=self.pk).exists(): # ty:ignore[unresolved-attribute]
|
|
29
56
|
slug = f"{base}-{n}"
|
|
30
57
|
n += 1
|
|
31
58
|
self.slug = slug
|
|
@@ -41,16 +68,44 @@ class Recipe(models.Model):
|
|
|
41
68
|
description = models.TextField(blank=True)
|
|
42
69
|
ingredients = models.TextField(blank=True)
|
|
43
70
|
instructions = models.TextField(blank=True)
|
|
71
|
+
uploaded_by = models.ForeignKey(
|
|
72
|
+
User,
|
|
73
|
+
related_name="recipes",
|
|
74
|
+
on_delete=models.SET_NULL,
|
|
75
|
+
null=True,
|
|
76
|
+
blank=True,
|
|
77
|
+
)
|
|
44
78
|
image = models.ImageField(
|
|
45
|
-
upload_to="recipes/",
|
|
79
|
+
upload_to="recipes/",
|
|
46
80
|
storage=hashed_storage,
|
|
47
81
|
blank=True,
|
|
48
82
|
null=True,
|
|
49
83
|
)
|
|
50
|
-
|
|
51
|
-
|
|
84
|
+
image_thumbnail = ImageSpecField(
|
|
85
|
+
source="image",
|
|
86
|
+
processors=[ResizeToFill(150, 150)],
|
|
87
|
+
format="JPEG",
|
|
88
|
+
options={"quality": 70},
|
|
89
|
+
)
|
|
90
|
+
image_small = ImageSpecField(
|
|
91
|
+
source="image",
|
|
92
|
+
processors=[ResizeToFill(400, 300)],
|
|
93
|
+
format="JPEG",
|
|
94
|
+
options={"quality": 75},
|
|
95
|
+
)
|
|
96
|
+
image_medium = ImageSpecField(
|
|
97
|
+
source="image",
|
|
98
|
+
processors=[ResizeToFill(700, 500)],
|
|
99
|
+
format="JPEG",
|
|
100
|
+
options={"quality": 85},
|
|
101
|
+
)
|
|
102
|
+
image_large = ImageSpecField(
|
|
103
|
+
source="image",
|
|
104
|
+
processors=[ResizeToFill(1200, 800)],
|
|
105
|
+
format="JPEG",
|
|
106
|
+
options={"quality": 95},
|
|
107
|
+
)
|
|
52
108
|
tags = models.ManyToManyField(Tag, blank=True, related_name="recipes")
|
|
53
|
-
|
|
54
109
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
55
110
|
updated_at = models.DateTimeField(auto_now=True)
|
|
56
111
|
history = HistoricalRecords()
|
|
@@ -61,19 +116,39 @@ class Recipe(models.Model):
|
|
|
61
116
|
verbose_name_plural = "Recipes"
|
|
62
117
|
|
|
63
118
|
def save(self, *args, **kwargs):
|
|
119
|
+
is_new = self._state.adding
|
|
120
|
+
|
|
64
121
|
if not self.slug:
|
|
65
122
|
base = slugify(self.title)[:240]
|
|
66
123
|
slug = base
|
|
67
124
|
n = 1
|
|
68
|
-
while Recipe.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
|
125
|
+
while Recipe.objects.filter(slug=slug).exclude(pk=self.pk).exists(): # ty:ignore[unresolved-attribute]
|
|
69
126
|
slug = f"{base}-{n}"
|
|
70
127
|
n += 1
|
|
71
128
|
self.slug = slug
|
|
129
|
+
|
|
72
130
|
super().save(*args, **kwargs)
|
|
73
131
|
|
|
132
|
+
send_email = getattr(settings, "SEND_EMAIL")
|
|
133
|
+
logging.debug(f"SEND_EMAIL is set to {send_email}")
|
|
134
|
+
|
|
135
|
+
if is_new or settings.DEBUG:
|
|
136
|
+
if send_email:
|
|
137
|
+
email_users.enqueue(recipe_id=self.pk)
|
|
138
|
+
else:
|
|
139
|
+
logging.warning(
|
|
140
|
+
"Email sending is disabled; not sending email notification, make sure SEND_EMAIL is set to True in settings."
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
logging.debug(
|
|
144
|
+
"Existing recipe saved (update); skipping email notification."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def get_absolute_url(self):
|
|
148
|
+
return reverse("recipe_detail", kwargs={"slug": self.slug})
|
|
149
|
+
|
|
74
150
|
def tag_list(self):
|
|
75
|
-
|
|
76
|
-
return list(self.tags.values_list("name", flat=True))
|
|
151
|
+
return list(self.tags.values_list("name", flat=True)) # ty:ignore[possibly-missing-attribute]
|
|
77
152
|
|
|
78
153
|
def set_tags_from_string(self, tag_string):
|
|
79
154
|
"""
|
|
@@ -83,24 +158,19 @@ class Recipe(models.Model):
|
|
|
83
158
|
names = [t.strip() for t in (tag_string or "").split(",") if t.strip()]
|
|
84
159
|
tags = []
|
|
85
160
|
for name in names:
|
|
86
|
-
tag = Tag.objects.filter(name__iexact=name).first()
|
|
161
|
+
tag = Tag.objects.filter(name__iexact=name).first() # ty:ignore[unresolved-attribute]
|
|
87
162
|
if not tag:
|
|
88
|
-
tag = Tag.objects.create(name=name)
|
|
163
|
+
tag = Tag.objects.create(name=name) # ty:ignore[unresolved-attribute]
|
|
89
164
|
tags.append(tag)
|
|
90
|
-
#
|
|
91
|
-
self.tags.
|
|
92
|
-
return self.tags.all()
|
|
165
|
+
self.tags.set(tags) # ty:ignore[possibly-missing-attribute]
|
|
166
|
+
return self.tags.all() # ty:ignore[possibly-missing-attribute]
|
|
93
167
|
|
|
94
|
-
# add helper methods for ratings
|
|
95
168
|
def average_rating(self):
|
|
96
|
-
agg = self.ratings.aggregate(avg=Avg("score"))
|
|
169
|
+
agg = self.ratings.aggregate(avg=Avg("score")) # ty:ignore[unresolved-attribute]
|
|
97
170
|
return agg["avg"] or 0
|
|
98
171
|
|
|
99
172
|
def rating_count(self):
|
|
100
|
-
return self.ratings.count()
|
|
101
|
-
|
|
102
|
-
def get_absolute_url(self):
|
|
103
|
-
return reverse("recipe_detail", kwargs={"pk": self.pk, "slug": self.slug})
|
|
173
|
+
return self.ratings.count() # ty:ignore[unresolved-attribute]
|
|
104
174
|
|
|
105
175
|
def __str__(self):
|
|
106
176
|
return self.title
|