sandwitches 1.4.2__py3-none-any.whl → 2.0.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 +21 -2
- sandwitches/api.py +112 -6
- sandwitches/feeds.py +23 -0
- sandwitches/forms.py +110 -7
- sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches/locale/nl/LC_MESSAGES/django.po +784 -134
- sandwitches/migrations/0001_initial.py +255 -2
- 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/models.py +48 -4
- sandwitches/settings.py +14 -5
- sandwitches/storage.py +44 -12
- 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 +80 -1
- sandwitches/templates/base_beer.html +57 -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 +167 -110
- sandwitches/templates/favorites.html +42 -0
- sandwitches/templates/index.html +28 -61
- sandwitches/templates/partials/recipe_list.html +87 -0
- sandwitches/templates/recipe_form.html +119 -0
- sandwitches/templates/setup.html +1 -1
- sandwitches/templates/signup.html +114 -31
- sandwitches/templatetags/custom_filters.py +15 -0
- sandwitches/urls.py +56 -0
- sandwitches/utils.py +222 -0
- sandwitches/views.py +503 -14
- sandwitches-2.0.0.dist-info/METADATA +104 -0
- sandwitches-2.0.0.dist-info/RECORD +62 -0
- sandwitches/migrations/0002_historicalrecipe.py +0 -61
- sandwitches/migrations/0003_rating.py +0 -57
- sandwitches/migrations/0004_add_uploaded_by.py +0 -25
- sandwitches/migrations/0005_historicalrecipe_uploaded_by.py +0 -27
- sandwitches/migrations/0006_profile.py +0 -48
- sandwitches/migrations/0007_alter_rating_score.py +0 -23
- sandwitches/migrations/0008_delete_profile.py +0 -15
- sandwitches/templates/base_pico.html +0 -260
- sandwitches/templates/form.html +0 -16
- sandwitches-1.4.2.dist-info/METADATA +0 -25
- sandwitches-1.4.2.dist-info/RECORD +0 -35
- {sandwitches-1.4.2.dist-info → sandwitches-2.0.0.dist-info}/WHEEL +0 -0
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
# Generated by Django
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-31 16:09
|
|
2
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
|
|
3
8
|
import sandwitches.storage
|
|
9
|
+
import simple_history.models
|
|
10
|
+
from django.conf import settings
|
|
4
11
|
from django.db import migrations, models
|
|
5
12
|
|
|
6
13
|
|
|
7
14
|
class Migration(migrations.Migration):
|
|
8
15
|
initial = True
|
|
9
16
|
|
|
10
|
-
dependencies = [
|
|
17
|
+
dependencies = [
|
|
18
|
+
("auth", "0012_alter_user_first_name_max_length"),
|
|
19
|
+
]
|
|
11
20
|
|
|
12
21
|
operations = [
|
|
13
22
|
migrations.CreateModel(
|
|
@@ -31,6 +40,188 @@ class Migration(migrations.Migration):
|
|
|
31
40
|
"ordering": ("name",),
|
|
32
41
|
},
|
|
33
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
|
+
),
|
|
34
225
|
migrations.CreateModel(
|
|
35
226
|
name="Recipe",
|
|
36
227
|
fields=[
|
|
@@ -59,6 +250,16 @@ class Migration(migrations.Migration):
|
|
|
59
250
|
),
|
|
60
251
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
61
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
|
+
),
|
|
62
263
|
(
|
|
63
264
|
"tags",
|
|
64
265
|
models.ManyToManyField(
|
|
@@ -72,4 +273,56 @@ class Migration(migrations.Migration):
|
|
|
72
273
|
"ordering": ("-created_at",),
|
|
73
274
|
},
|
|
74
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
|
+
),
|
|
75
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
|
+
]
|
sandwitches/models.py
CHANGED
|
@@ -2,20 +2,60 @@ from django.db import models
|
|
|
2
2
|
from django.utils.text import slugify
|
|
3
3
|
from .storage import HashedFilenameStorage
|
|
4
4
|
from simple_history.models import HistoricalRecords
|
|
5
|
-
from django.contrib.auth import
|
|
5
|
+
from django.contrib.auth.models import AbstractUser
|
|
6
6
|
from django.db.models import Avg
|
|
7
7
|
from .tasks import email_users
|
|
8
8
|
from django.conf import settings
|
|
9
9
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
10
10
|
import logging
|
|
11
11
|
from django.urls import reverse
|
|
12
|
+
from solo.models import SingletonModel
|
|
12
13
|
|
|
13
14
|
from imagekit.models import ImageSpecField
|
|
14
15
|
from imagekit.processors import ResizeToFill
|
|
15
16
|
|
|
16
17
|
hashed_storage = HashedFilenameStorage()
|
|
17
18
|
|
|
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
|
|
19
59
|
|
|
20
60
|
|
|
21
61
|
class Tag(models.Model):
|
|
@@ -48,8 +88,9 @@ class Recipe(models.Model):
|
|
|
48
88
|
description = models.TextField(blank=True)
|
|
49
89
|
ingredients = models.TextField(blank=True)
|
|
50
90
|
instructions = models.TextField(blank=True)
|
|
91
|
+
servings = models.IntegerField(default=1, validators=[MinValueValidator(1)])
|
|
51
92
|
uploaded_by = models.ForeignKey(
|
|
52
|
-
|
|
93
|
+
settings.AUTH_USER_MODEL,
|
|
53
94
|
related_name="recipes",
|
|
54
95
|
on_delete=models.SET_NULL,
|
|
55
96
|
null=True,
|
|
@@ -158,10 +199,13 @@ class Recipe(models.Model):
|
|
|
158
199
|
|
|
159
200
|
class Rating(models.Model):
|
|
160
201
|
recipe = models.ForeignKey(Recipe, related_name="ratings", on_delete=models.CASCADE)
|
|
161
|
-
user = models.ForeignKey(
|
|
202
|
+
user = models.ForeignKey(
|
|
203
|
+
settings.AUTH_USER_MODEL, related_name="ratings", on_delete=models.CASCADE
|
|
204
|
+
)
|
|
162
205
|
score = models.FloatField(
|
|
163
206
|
validators=[MinValueValidator(0.0), MaxValueValidator(10.0)]
|
|
164
207
|
)
|
|
208
|
+
comment = models.TextField(blank=True)
|
|
165
209
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
166
210
|
updated_at = models.DateTimeField(auto_now=True)
|
|
167
211
|
|
sandwitches/settings.py
CHANGED
|
@@ -12,10 +12,11 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
|
|
12
12
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
import os
|
|
15
|
+
|
|
15
16
|
from django.core.exceptions import ImproperlyConfigured
|
|
16
17
|
from . import storage
|
|
17
18
|
|
|
18
|
-
DEBUG = bool(os.environ.get("DEBUG", default=0))
|
|
19
|
+
DEBUG = bool(os.environ.get("DEBUG", default=0))
|
|
19
20
|
|
|
20
21
|
SECRET_KEY = os.environ.get("SECRET_KEY")
|
|
21
22
|
if not SECRET_KEY:
|
|
@@ -25,7 +26,7 @@ if not SECRET_KEY:
|
|
|
25
26
|
|
|
26
27
|
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "127.0.0.1, localhost").split(",")
|
|
27
28
|
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",")
|
|
28
|
-
DATABASE_FILE = Path(os.environ.get("DATABASE_FILE", default="/db/db.sqlite3"))
|
|
29
|
+
DATABASE_FILE = Path(os.environ.get("DATABASE_FILE", default="/db/db.sqlite3"))
|
|
29
30
|
|
|
30
31
|
storage.is_database_readable(DATABASE_FILE)
|
|
31
32
|
storage.is_database_writable(DATABASE_FILE)
|
|
@@ -67,6 +68,7 @@ INSTALLED_APPS = [
|
|
|
67
68
|
"imagekit",
|
|
68
69
|
"import_export",
|
|
69
70
|
"simple_history",
|
|
71
|
+
"solo",
|
|
70
72
|
]
|
|
71
73
|
|
|
72
74
|
MIDDLEWARE = [
|
|
@@ -132,6 +134,8 @@ LOGGING = {
|
|
|
132
134
|
# Password validation
|
|
133
135
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
|
134
136
|
|
|
137
|
+
AUTH_USER_MODEL = "sandwitches.User"
|
|
138
|
+
|
|
135
139
|
AUTH_PASSWORD_VALIDATORS = [
|
|
136
140
|
{
|
|
137
141
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
@@ -150,13 +154,11 @@ AUTH_PASSWORD_VALIDATORS = [
|
|
|
150
154
|
|
|
151
155
|
# Media files (for uploaded images)
|
|
152
156
|
MEDIA_URL = "/media/"
|
|
153
|
-
MEDIA_ROOT = Path(os.environ.get("MEDIA_ROOT", default=BASE_DIR / "media"))
|
|
157
|
+
MEDIA_ROOT = Path(os.environ.get("MEDIA_ROOT", default=BASE_DIR / "media"))
|
|
154
158
|
|
|
155
159
|
# Static (for CSS etc)
|
|
156
160
|
STATIC_URL = "/static/"
|
|
157
161
|
STATIC_ROOT = Path("/tmp/staticfiles")
|
|
158
|
-
STATIC_URL = "static/"
|
|
159
|
-
|
|
160
162
|
STATICFILES_DIRS = [BASE_DIR / "static", MEDIA_ROOT]
|
|
161
163
|
|
|
162
164
|
LANGUAGE_CODE = "en"
|
|
@@ -171,6 +173,12 @@ LOCALE_PATHS = [BASE_DIR / "locale"]
|
|
|
171
173
|
|
|
172
174
|
USE_TZ = True
|
|
173
175
|
|
|
176
|
+
# EU Date formats
|
|
177
|
+
DATE_FORMAT = "d/m/Y"
|
|
178
|
+
DATETIME_FORMAT = "d/m/Y H:i:s"
|
|
179
|
+
SHORT_DATE_FORMAT = "d/m/Y"
|
|
180
|
+
SHORT_DATETIME_FORMAT = "d/m/Y H:i"
|
|
181
|
+
|
|
174
182
|
INTERNAL_IPS = [
|
|
175
183
|
"127.0.0.1",
|
|
176
184
|
]
|
|
@@ -184,6 +192,7 @@ STORAGES = {
|
|
|
184
192
|
},
|
|
185
193
|
}
|
|
186
194
|
|
|
195
|
+
|
|
187
196
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
|
188
197
|
EMAIL_USE_TLS = os.environ.get("SMTP_USE_TLS")
|
|
189
198
|
EMAIL_HOST = os.environ.get("SMTP_HOST")
|
sandwitches/storage.py
CHANGED
|
@@ -46,13 +46,21 @@ def is_database_readable(path=None) -> bool:
|
|
|
46
46
|
|
|
47
47
|
p = Path(path)
|
|
48
48
|
logging.debug(f"Checking database file readability at: {p}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
try:
|
|
50
|
+
if p.is_file():
|
|
51
|
+
with open(p, "r"): # Removed 'as f'
|
|
52
|
+
# If we can open it, it's readable. No need to read content.
|
|
53
|
+
pass
|
|
54
|
+
logging.debug(f"Database file at {p} is readable.")
|
|
55
|
+
return True
|
|
56
|
+
else:
|
|
57
|
+
logging.error(f"Database file at {p} does not exist.")
|
|
58
|
+
return False
|
|
59
|
+
except IOError:
|
|
60
|
+
logging.error(
|
|
61
|
+
f"Database file at {p} is not readable due to permission or other IO error."
|
|
62
|
+
)
|
|
52
63
|
return False
|
|
53
|
-
else:
|
|
54
|
-
logging.debug(f"Database file at {p} is readable.")
|
|
55
|
-
return readable
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
def is_database_writable(path=None) -> bool:
|
|
@@ -73,10 +81,34 @@ def is_database_writable(path=None) -> bool:
|
|
|
73
81
|
|
|
74
82
|
p = Path(path)
|
|
75
83
|
logging.debug(f"Checking database file writability at: {p}")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
try:
|
|
85
|
+
# If the file exists, try to open it for appending
|
|
86
|
+
if p.is_file():
|
|
87
|
+
with open(p, "a"): # Removed 'as f'
|
|
88
|
+
pass
|
|
89
|
+
logging.debug(f"Database file at {p} is writable.")
|
|
90
|
+
return True
|
|
91
|
+
else:
|
|
92
|
+
# If file does not exist, check if its parent directory is writable
|
|
93
|
+
if p.parent.is_dir() and os.access(p.parent, os.W_OK):
|
|
94
|
+
# Try creating a dummy file to confirm writability
|
|
95
|
+
dummy_file = p.parent / f".tmp_writable_test_{os.getpid()}"
|
|
96
|
+
try:
|
|
97
|
+
dummy_file.touch()
|
|
98
|
+
dummy_file.unlink()
|
|
99
|
+
logging.debug(f"Database path at {p.parent} is writable.")
|
|
100
|
+
return True
|
|
101
|
+
except IOError:
|
|
102
|
+
logging.error(f"Cannot create dummy file in {p.parent}.")
|
|
103
|
+
return False
|
|
104
|
+
else:
|
|
105
|
+
logging.error(
|
|
106
|
+
f"Parent directory {p.parent} is not writable or does not exist."
|
|
107
|
+
)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
except IOError:
|
|
111
|
+
logging.error(
|
|
112
|
+
f"Database file at {p} is not writable due to permission or other IO error."
|
|
113
|
+
)
|
|
79
114
|
return False
|
|
80
|
-
else:
|
|
81
|
-
logging.debug(f"Database file at {p} is writable.")
|
|
82
|
-
return writable
|