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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-31 16:09
|
|
2
|
+
|
|
3
|
+
import django.contrib.auth.models
|
|
4
|
+
import django.contrib.auth.validators
|
|
5
|
+
import django.core.validators
|
|
6
|
+
import django.db.models.deletion
|
|
7
|
+
import django.utils.timezone
|
|
8
|
+
import sandwitches.storage
|
|
9
|
+
import simple_history.models
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.db import migrations, models
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Migration(migrations.Migration):
|
|
15
|
+
initial = True
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
("auth", "0012_alter_user_first_name_max_length"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
operations = [
|
|
22
|
+
migrations.CreateModel(
|
|
23
|
+
name="Tag",
|
|
24
|
+
fields=[
|
|
25
|
+
(
|
|
26
|
+
"id",
|
|
27
|
+
models.BigAutoField(
|
|
28
|
+
auto_created=True,
|
|
29
|
+
primary_key=True,
|
|
30
|
+
serialize=False,
|
|
31
|
+
verbose_name="ID",
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
("name", models.CharField(max_length=50, unique=True)),
|
|
35
|
+
("slug", models.SlugField(blank=True, max_length=60, unique=True)),
|
|
36
|
+
],
|
|
37
|
+
options={
|
|
38
|
+
"verbose_name": "Tag",
|
|
39
|
+
"verbose_name_plural": "Tags",
|
|
40
|
+
"ordering": ("name",),
|
|
41
|
+
},
|
|
42
|
+
),
|
|
43
|
+
migrations.CreateModel(
|
|
44
|
+
name="User",
|
|
45
|
+
fields=[
|
|
46
|
+
(
|
|
47
|
+
"id",
|
|
48
|
+
models.BigAutoField(
|
|
49
|
+
auto_created=True,
|
|
50
|
+
primary_key=True,
|
|
51
|
+
serialize=False,
|
|
52
|
+
verbose_name="ID",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
("password", models.CharField(max_length=128, verbose_name="password")),
|
|
56
|
+
(
|
|
57
|
+
"last_login",
|
|
58
|
+
models.DateTimeField(
|
|
59
|
+
blank=True, null=True, verbose_name="last login"
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
"is_superuser",
|
|
64
|
+
models.BooleanField(
|
|
65
|
+
default=False,
|
|
66
|
+
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
|
67
|
+
verbose_name="superuser status",
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
(
|
|
71
|
+
"username",
|
|
72
|
+
models.CharField(
|
|
73
|
+
error_messages={
|
|
74
|
+
"unique": "A user with that username already exists."
|
|
75
|
+
},
|
|
76
|
+
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
|
77
|
+
max_length=150,
|
|
78
|
+
unique=True,
|
|
79
|
+
validators=[
|
|
80
|
+
django.contrib.auth.validators.UnicodeUsernameValidator()
|
|
81
|
+
],
|
|
82
|
+
verbose_name="username",
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
"first_name",
|
|
87
|
+
models.CharField(
|
|
88
|
+
blank=True, max_length=150, verbose_name="first name"
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
"last_name",
|
|
93
|
+
models.CharField(
|
|
94
|
+
blank=True, max_length=150, verbose_name="last name"
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
(
|
|
98
|
+
"email",
|
|
99
|
+
models.EmailField(
|
|
100
|
+
blank=True, max_length=254, verbose_name="email address"
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
"is_staff",
|
|
105
|
+
models.BooleanField(
|
|
106
|
+
default=False,
|
|
107
|
+
help_text="Designates whether the user can log into this admin site.",
|
|
108
|
+
verbose_name="staff status",
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
"is_active",
|
|
113
|
+
models.BooleanField(
|
|
114
|
+
default=True,
|
|
115
|
+
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
|
116
|
+
verbose_name="active",
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
(
|
|
120
|
+
"date_joined",
|
|
121
|
+
models.DateTimeField(
|
|
122
|
+
default=django.utils.timezone.now, verbose_name="date joined"
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
"avatar",
|
|
127
|
+
models.ImageField(blank=True, null=True, upload_to="avatars"),
|
|
128
|
+
),
|
|
129
|
+
("bio", models.TextField(blank=True)),
|
|
130
|
+
(
|
|
131
|
+
"language",
|
|
132
|
+
models.CharField(
|
|
133
|
+
choices=[("en", "English"), ("nl", "Nederlands")],
|
|
134
|
+
default="en",
|
|
135
|
+
max_length=10,
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
(
|
|
139
|
+
"groups",
|
|
140
|
+
models.ManyToManyField(
|
|
141
|
+
blank=True,
|
|
142
|
+
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
|
143
|
+
related_name="user_set",
|
|
144
|
+
related_query_name="user",
|
|
145
|
+
to="auth.group",
|
|
146
|
+
verbose_name="groups",
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
(
|
|
150
|
+
"user_permissions",
|
|
151
|
+
models.ManyToManyField(
|
|
152
|
+
blank=True,
|
|
153
|
+
help_text="Specific permissions for this user.",
|
|
154
|
+
related_name="user_set",
|
|
155
|
+
related_query_name="user",
|
|
156
|
+
to="auth.permission",
|
|
157
|
+
verbose_name="user permissions",
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
],
|
|
161
|
+
options={
|
|
162
|
+
"verbose_name": "User",
|
|
163
|
+
"verbose_name_plural": "Users",
|
|
164
|
+
},
|
|
165
|
+
managers=[
|
|
166
|
+
("objects", django.contrib.auth.models.UserManager()),
|
|
167
|
+
],
|
|
168
|
+
),
|
|
169
|
+
migrations.CreateModel(
|
|
170
|
+
name="HistoricalRecipe",
|
|
171
|
+
fields=[
|
|
172
|
+
(
|
|
173
|
+
"id",
|
|
174
|
+
models.BigIntegerField(
|
|
175
|
+
auto_created=True, blank=True, db_index=True, verbose_name="ID"
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
("title", models.CharField(db_index=True, max_length=255)),
|
|
179
|
+
("slug", models.SlugField(blank=True, max_length=255)),
|
|
180
|
+
("description", models.TextField(blank=True)),
|
|
181
|
+
("ingredients", models.TextField(blank=True)),
|
|
182
|
+
("instructions", models.TextField(blank=True)),
|
|
183
|
+
("image", models.TextField(blank=True, max_length=100, null=True)),
|
|
184
|
+
("created_at", models.DateTimeField(blank=True, editable=False)),
|
|
185
|
+
("updated_at", models.DateTimeField(blank=True, editable=False)),
|
|
186
|
+
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
|
187
|
+
("history_date", models.DateTimeField(db_index=True)),
|
|
188
|
+
("history_change_reason", models.CharField(max_length=100, null=True)),
|
|
189
|
+
(
|
|
190
|
+
"history_type",
|
|
191
|
+
models.CharField(
|
|
192
|
+
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
|
193
|
+
max_length=1,
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
(
|
|
197
|
+
"history_user",
|
|
198
|
+
models.ForeignKey(
|
|
199
|
+
null=True,
|
|
200
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
201
|
+
related_name="+",
|
|
202
|
+
to=settings.AUTH_USER_MODEL,
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
(
|
|
206
|
+
"uploaded_by",
|
|
207
|
+
models.ForeignKey(
|
|
208
|
+
blank=True,
|
|
209
|
+
db_constraint=False,
|
|
210
|
+
null=True,
|
|
211
|
+
on_delete=django.db.models.deletion.DO_NOTHING,
|
|
212
|
+
related_name="+",
|
|
213
|
+
to=settings.AUTH_USER_MODEL,
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
],
|
|
217
|
+
options={
|
|
218
|
+
"verbose_name": "historical Recipe",
|
|
219
|
+
"verbose_name_plural": "historical Recipes",
|
|
220
|
+
"ordering": ("-history_date", "-history_id"),
|
|
221
|
+
"get_latest_by": ("history_date", "history_id"),
|
|
222
|
+
},
|
|
223
|
+
bases=(simple_history.models.HistoricalChanges, models.Model),
|
|
224
|
+
),
|
|
225
|
+
migrations.CreateModel(
|
|
226
|
+
name="Recipe",
|
|
227
|
+
fields=[
|
|
228
|
+
(
|
|
229
|
+
"id",
|
|
230
|
+
models.BigAutoField(
|
|
231
|
+
auto_created=True,
|
|
232
|
+
primary_key=True,
|
|
233
|
+
serialize=False,
|
|
234
|
+
verbose_name="ID",
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
("title", models.CharField(max_length=255, unique=True)),
|
|
238
|
+
("slug", models.SlugField(blank=True, max_length=255, unique=True)),
|
|
239
|
+
("description", models.TextField(blank=True)),
|
|
240
|
+
("ingredients", models.TextField(blank=True)),
|
|
241
|
+
("instructions", models.TextField(blank=True)),
|
|
242
|
+
(
|
|
243
|
+
"image",
|
|
244
|
+
models.ImageField(
|
|
245
|
+
blank=True,
|
|
246
|
+
null=True,
|
|
247
|
+
storage=sandwitches.storage.HashedFilenameStorage(),
|
|
248
|
+
upload_to="recipes/",
|
|
249
|
+
),
|
|
250
|
+
),
|
|
251
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
252
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
253
|
+
(
|
|
254
|
+
"uploaded_by",
|
|
255
|
+
models.ForeignKey(
|
|
256
|
+
blank=True,
|
|
257
|
+
null=True,
|
|
258
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
259
|
+
related_name="recipes",
|
|
260
|
+
to=settings.AUTH_USER_MODEL,
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
(
|
|
264
|
+
"tags",
|
|
265
|
+
models.ManyToManyField(
|
|
266
|
+
blank=True, related_name="recipes", to="sandwitches.tag"
|
|
267
|
+
),
|
|
268
|
+
),
|
|
269
|
+
],
|
|
270
|
+
options={
|
|
271
|
+
"verbose_name": "Recipe",
|
|
272
|
+
"verbose_name_plural": "Recipes",
|
|
273
|
+
"ordering": ("-created_at",),
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
migrations.AddField(
|
|
277
|
+
model_name="user",
|
|
278
|
+
name="favorites",
|
|
279
|
+
field=models.ManyToManyField(
|
|
280
|
+
blank=True, related_name="favorited_by", to="sandwitches.recipe"
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
migrations.CreateModel(
|
|
284
|
+
name="Rating",
|
|
285
|
+
fields=[
|
|
286
|
+
(
|
|
287
|
+
"id",
|
|
288
|
+
models.BigAutoField(
|
|
289
|
+
auto_created=True,
|
|
290
|
+
primary_key=True,
|
|
291
|
+
serialize=False,
|
|
292
|
+
verbose_name="ID",
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
(
|
|
296
|
+
"score",
|
|
297
|
+
models.FloatField(
|
|
298
|
+
validators=[
|
|
299
|
+
django.core.validators.MinValueValidator(0.0),
|
|
300
|
+
django.core.validators.MaxValueValidator(10.0),
|
|
301
|
+
]
|
|
302
|
+
),
|
|
303
|
+
),
|
|
304
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
305
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
306
|
+
(
|
|
307
|
+
"user",
|
|
308
|
+
models.ForeignKey(
|
|
309
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
310
|
+
related_name="ratings",
|
|
311
|
+
to=settings.AUTH_USER_MODEL,
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
(
|
|
315
|
+
"recipe",
|
|
316
|
+
models.ForeignKey(
|
|
317
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
318
|
+
related_name="ratings",
|
|
319
|
+
to="sandwitches.recipe",
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
],
|
|
323
|
+
options={
|
|
324
|
+
"ordering": ("-updated_at",),
|
|
325
|
+
"unique_together": {("recipe", "user")},
|
|
326
|
+
},
|
|
327
|
+
),
|
|
328
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2026-01-13 11:59
|
|
2
|
+
|
|
3
|
+
import django.core.validators
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("sandwitches", "0001_initial"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name="historicalrecipe",
|
|
15
|
+
name="servings",
|
|
16
|
+
field=models.IntegerField(
|
|
17
|
+
default=1, validators=[django.core.validators.MinValueValidator(1)]
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
migrations.AddField(
|
|
21
|
+
model_name="recipe",
|
|
22
|
+
name="servings",
|
|
23
|
+
field=models.IntegerField(
|
|
24
|
+
default=1, validators=[django.core.validators.MinValueValidator(1)]
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2026-01-14 09:13
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0002_historicalrecipe_servings_recipe_servings"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.CreateModel(
|
|
13
|
+
name="Setting",
|
|
14
|
+
fields=[
|
|
15
|
+
(
|
|
16
|
+
"id",
|
|
17
|
+
models.BigAutoField(
|
|
18
|
+
auto_created=True,
|
|
19
|
+
primary_key=True,
|
|
20
|
+
serialize=False,
|
|
21
|
+
verbose_name="ID",
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
("site_name", models.CharField(default="Sandwitches", max_length=255)),
|
|
25
|
+
("site_description", models.TextField(blank=True)),
|
|
26
|
+
("email", models.EmailField(blank=True, max_length=254)),
|
|
27
|
+
("ai_connection_point", models.URLField(blank=True)),
|
|
28
|
+
("ai_model", models.CharField(blank=True, max_length=255)),
|
|
29
|
+
("ai_api_key", models.CharField(blank=True, max_length=255)),
|
|
30
|
+
],
|
|
31
|
+
options={
|
|
32
|
+
"verbose_name": "Site Settings",
|
|
33
|
+
},
|
|
34
|
+
),
|
|
35
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2026-01-14 09:32
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0003_setting"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AlterField(
|
|
13
|
+
model_name="setting",
|
|
14
|
+
name="ai_api_key",
|
|
15
|
+
field=models.CharField(blank=True, max_length=255, null=True),
|
|
16
|
+
),
|
|
17
|
+
migrations.AlterField(
|
|
18
|
+
model_name="setting",
|
|
19
|
+
name="ai_connection_point",
|
|
20
|
+
field=models.URLField(blank=True, null=True),
|
|
21
|
+
),
|
|
22
|
+
migrations.AlterField(
|
|
23
|
+
model_name="setting",
|
|
24
|
+
name="ai_model",
|
|
25
|
+
field=models.CharField(blank=True, max_length=255, null=True),
|
|
26
|
+
),
|
|
27
|
+
migrations.AlterField(
|
|
28
|
+
model_name="setting",
|
|
29
|
+
name="email",
|
|
30
|
+
field=models.EmailField(blank=True, max_length=254, null=True),
|
|
31
|
+
),
|
|
32
|
+
migrations.AlterField(
|
|
33
|
+
model_name="setting",
|
|
34
|
+
name="site_description",
|
|
35
|
+
field=models.TextField(blank=True, null=True),
|
|
36
|
+
),
|
|
37
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2026-01-14 10:03
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0004_alter_setting_ai_api_key_and_more"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="rating",
|
|
14
|
+
name="comment",
|
|
15
|
+
field=models.TextField(blank=True),
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 6.0.1 on 2026-01-17 16:48
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("sandwitches", "0005_rating_comment"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="historicalrecipe",
|
|
14
|
+
name="is_highlighted",
|
|
15
|
+
field=models.BooleanField(default=False),
|
|
16
|
+
),
|
|
17
|
+
migrations.AddField(
|
|
18
|
+
model_name="recipe",
|
|
19
|
+
name="is_highlighted",
|
|
20
|
+
field=models.BooleanField(default=False),
|
|
21
|
+
),
|
|
22
|
+
]
|
|
File without changes
|
sandwitches/models.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.text import slugify
|
|
3
|
+
from .storage import HashedFilenameStorage
|
|
4
|
+
from simple_history.models import HistoricalRecords
|
|
5
|
+
from django.contrib.auth.models import AbstractUser
|
|
6
|
+
from django.db.models import Avg
|
|
7
|
+
from .tasks import email_users
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
10
|
+
import logging
|
|
11
|
+
from django.urls import reverse
|
|
12
|
+
from solo.models import SingletonModel
|
|
13
|
+
|
|
14
|
+
from imagekit.models import ImageSpecField
|
|
15
|
+
from imagekit.processors import ResizeToFill
|
|
16
|
+
|
|
17
|
+
hashed_storage = HashedFilenameStorage()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Setting(SingletonModel):
|
|
21
|
+
site_name = models.CharField(max_length=255, default="Sandwitches")
|
|
22
|
+
site_description = models.TextField(blank=True, null=True)
|
|
23
|
+
email = models.EmailField(blank=True, null=True)
|
|
24
|
+
ai_connection_point = models.URLField(blank=True, null=True)
|
|
25
|
+
ai_model = models.CharField(max_length=255, blank=True, null=True)
|
|
26
|
+
ai_api_key = models.CharField(max_length=255, blank=True, null=True)
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return "Site Settings"
|
|
30
|
+
|
|
31
|
+
class Meta:
|
|
32
|
+
verbose_name = "Site Settings"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class User(AbstractUser):
|
|
36
|
+
avatar = models.ImageField(upload_to="avatars", blank=True, null=True)
|
|
37
|
+
avatar_thumbnail = ImageSpecField(
|
|
38
|
+
source="avatar",
|
|
39
|
+
processors=[ResizeToFill(100, 50)],
|
|
40
|
+
format="JPEG",
|
|
41
|
+
options={"quality": 60},
|
|
42
|
+
)
|
|
43
|
+
bio = models.TextField(blank=True)
|
|
44
|
+
language = models.CharField(
|
|
45
|
+
max_length=10,
|
|
46
|
+
choices=settings.LANGUAGES,
|
|
47
|
+
default=settings.LANGUAGE_CODE,
|
|
48
|
+
)
|
|
49
|
+
favorites = models.ManyToManyField(
|
|
50
|
+
"Recipe", related_name="favorited_by", blank=True
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
class Meta:
|
|
54
|
+
verbose_name = "User"
|
|
55
|
+
verbose_name_plural = "Users"
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return self.username
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Tag(models.Model):
|
|
62
|
+
name = models.CharField(max_length=50, unique=True)
|
|
63
|
+
slug = models.SlugField(max_length=60, unique=True, blank=True)
|
|
64
|
+
|
|
65
|
+
class Meta:
|
|
66
|
+
ordering = ("name",)
|
|
67
|
+
verbose_name = "Tag"
|
|
68
|
+
verbose_name_plural = "Tags"
|
|
69
|
+
|
|
70
|
+
def save(self, *args, **kwargs):
|
|
71
|
+
if not self.slug:
|
|
72
|
+
base = slugify(self.name)[:55]
|
|
73
|
+
slug = base
|
|
74
|
+
n = 1
|
|
75
|
+
while Tag.objects.filter(slug=slug).exclude(pk=self.pk).exists(): # ty:ignore[unresolved-attribute]
|
|
76
|
+
slug = f"{base}-{n}"
|
|
77
|
+
n += 1
|
|
78
|
+
self.slug = slug
|
|
79
|
+
super().save(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
def __str__(self):
|
|
82
|
+
return self.name
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Recipe(models.Model):
|
|
86
|
+
title = models.CharField(max_length=255, unique=True)
|
|
87
|
+
slug = models.SlugField(max_length=255, unique=True, blank=True)
|
|
88
|
+
description = models.TextField(blank=True)
|
|
89
|
+
ingredients = models.TextField(blank=True)
|
|
90
|
+
instructions = models.TextField(blank=True)
|
|
91
|
+
servings = models.IntegerField(default=1, validators=[MinValueValidator(1)])
|
|
92
|
+
uploaded_by = models.ForeignKey(
|
|
93
|
+
settings.AUTH_USER_MODEL,
|
|
94
|
+
related_name="recipes",
|
|
95
|
+
on_delete=models.SET_NULL,
|
|
96
|
+
null=True,
|
|
97
|
+
blank=True,
|
|
98
|
+
)
|
|
99
|
+
image = models.ImageField(
|
|
100
|
+
upload_to="recipes/",
|
|
101
|
+
storage=hashed_storage,
|
|
102
|
+
blank=True,
|
|
103
|
+
null=True,
|
|
104
|
+
)
|
|
105
|
+
image_thumbnail = ImageSpecField(
|
|
106
|
+
source="image",
|
|
107
|
+
processors=[ResizeToFill(150, 150)],
|
|
108
|
+
format="JPEG",
|
|
109
|
+
options={"quality": 70},
|
|
110
|
+
)
|
|
111
|
+
image_small = ImageSpecField(
|
|
112
|
+
source="image",
|
|
113
|
+
processors=[ResizeToFill(400, 300)],
|
|
114
|
+
format="JPEG",
|
|
115
|
+
options={"quality": 75},
|
|
116
|
+
)
|
|
117
|
+
image_medium = ImageSpecField(
|
|
118
|
+
source="image",
|
|
119
|
+
processors=[ResizeToFill(700, 500)],
|
|
120
|
+
format="JPEG",
|
|
121
|
+
options={"quality": 85},
|
|
122
|
+
)
|
|
123
|
+
image_large = ImageSpecField(
|
|
124
|
+
source="image",
|
|
125
|
+
processors=[ResizeToFill(1200, 800)],
|
|
126
|
+
format="JPEG",
|
|
127
|
+
options={"quality": 95},
|
|
128
|
+
)
|
|
129
|
+
tags = models.ManyToManyField(Tag, blank=True, related_name="recipes")
|
|
130
|
+
is_highlighted = models.BooleanField(default=False)
|
|
131
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
132
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
133
|
+
history = HistoricalRecords()
|
|
134
|
+
|
|
135
|
+
class Meta:
|
|
136
|
+
ordering = ("-created_at",)
|
|
137
|
+
verbose_name = "Recipe"
|
|
138
|
+
verbose_name_plural = "Recipes"
|
|
139
|
+
|
|
140
|
+
def save(self, *args, **kwargs):
|
|
141
|
+
is_new = self._state.adding
|
|
142
|
+
|
|
143
|
+
if not self.slug:
|
|
144
|
+
base = slugify(self.title)[:240]
|
|
145
|
+
slug = base
|
|
146
|
+
n = 1
|
|
147
|
+
while Recipe.objects.filter(slug=slug).exclude(pk=self.pk).exists(): # ty:ignore[unresolved-attribute]
|
|
148
|
+
slug = f"{base}-{n}"
|
|
149
|
+
n += 1
|
|
150
|
+
self.slug = slug
|
|
151
|
+
|
|
152
|
+
super().save(*args, **kwargs)
|
|
153
|
+
|
|
154
|
+
send_email = getattr(settings, "SEND_EMAIL")
|
|
155
|
+
logging.debug(f"SEND_EMAIL is set to {send_email}")
|
|
156
|
+
|
|
157
|
+
if is_new or settings.DEBUG:
|
|
158
|
+
if send_email:
|
|
159
|
+
email_users.enqueue(recipe_id=self.pk)
|
|
160
|
+
else:
|
|
161
|
+
logging.warning(
|
|
162
|
+
"Email sending is disabled; not sending email notification, make sure SEND_EMAIL is set to True in settings."
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
logging.debug(
|
|
166
|
+
"Existing recipe saved (update); skipping email notification."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def get_absolute_url(self):
|
|
170
|
+
return reverse("recipe_detail", kwargs={"slug": self.slug})
|
|
171
|
+
|
|
172
|
+
def tag_list(self):
|
|
173
|
+
return list(self.tags.values_list("name", flat=True)) # ty:ignore[possibly-missing-attribute]
|
|
174
|
+
|
|
175
|
+
def set_tags_from_string(self, tag_string):
|
|
176
|
+
"""
|
|
177
|
+
Accepts a comma separated string like "tag1, tag2" and attaches existing tags
|
|
178
|
+
or creates new ones as needed. Returns the Tag queryset assigned.
|
|
179
|
+
"""
|
|
180
|
+
names = [t.strip() for t in (tag_string or "").split(",") if t.strip()]
|
|
181
|
+
tags = []
|
|
182
|
+
for name in names:
|
|
183
|
+
tag = Tag.objects.filter(name__iexact=name).first() # ty:ignore[unresolved-attribute]
|
|
184
|
+
if not tag:
|
|
185
|
+
tag = Tag.objects.create(name=name) # ty:ignore[unresolved-attribute]
|
|
186
|
+
tags.append(tag)
|
|
187
|
+
self.tags.set(tags) # ty:ignore[possibly-missing-attribute]
|
|
188
|
+
return self.tags.all() # ty:ignore[possibly-missing-attribute]
|
|
189
|
+
|
|
190
|
+
def average_rating(self):
|
|
191
|
+
agg = self.ratings.aggregate(avg=Avg("score")) # ty:ignore[unresolved-attribute]
|
|
192
|
+
return agg["avg"] or 0
|
|
193
|
+
|
|
194
|
+
def rating_count(self):
|
|
195
|
+
return self.ratings.count() # ty:ignore[unresolved-attribute]
|
|
196
|
+
|
|
197
|
+
def __str__(self):
|
|
198
|
+
return self.title
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Rating(models.Model):
|
|
202
|
+
recipe = models.ForeignKey(Recipe, related_name="ratings", on_delete=models.CASCADE)
|
|
203
|
+
user = models.ForeignKey(
|
|
204
|
+
settings.AUTH_USER_MODEL, related_name="ratings", on_delete=models.CASCADE
|
|
205
|
+
)
|
|
206
|
+
score = models.FloatField(
|
|
207
|
+
validators=[MinValueValidator(0.0), MaxValueValidator(10.0)]
|
|
208
|
+
)
|
|
209
|
+
comment = models.TextField(blank=True)
|
|
210
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
211
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
212
|
+
|
|
213
|
+
class Meta:
|
|
214
|
+
unique_together = ("recipe", "user")
|
|
215
|
+
ordering = ("-updated_at",)
|
|
216
|
+
|
|
217
|
+
def __str__(self):
|
|
218
|
+
return f"{self.recipe} — {self.score} by {self.user}"
|