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.
Files changed (67) hide show
  1. sandwitches/__init__.py +6 -0
  2. sandwitches/admin.py +21 -2
  3. sandwitches/api.py +112 -6
  4. sandwitches/feeds.py +23 -0
  5. sandwitches/forms.py +110 -7
  6. sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  7. sandwitches/locale/nl/LC_MESSAGES/django.po +784 -134
  8. sandwitches/migrations/0001_initial.py +255 -2
  9. sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
  10. sandwitches/migrations/0003_setting.py +35 -0
  11. sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
  12. sandwitches/migrations/0005_rating_comment.py +17 -0
  13. sandwitches/models.py +48 -4
  14. sandwitches/settings.py +14 -5
  15. sandwitches/storage.py +44 -12
  16. sandwitches/templates/admin/admin_base.html +118 -0
  17. sandwitches/templates/admin/confirm_delete.html +23 -0
  18. sandwitches/templates/admin/dashboard.html +262 -0
  19. sandwitches/templates/admin/rating_list.html +38 -0
  20. sandwitches/templates/admin/recipe_form.html +184 -0
  21. sandwitches/templates/admin/recipe_list.html +64 -0
  22. sandwitches/templates/admin/tag_form.html +30 -0
  23. sandwitches/templates/admin/tag_list.html +37 -0
  24. sandwitches/templates/admin/task_detail.html +91 -0
  25. sandwitches/templates/admin/task_list.html +41 -0
  26. sandwitches/templates/admin/user_form.html +37 -0
  27. sandwitches/templates/admin/user_list.html +60 -0
  28. sandwitches/templates/base.html +80 -1
  29. sandwitches/templates/base_beer.html +57 -0
  30. sandwitches/templates/components/favorites_search_form.html +85 -0
  31. sandwitches/templates/components/footer.html +14 -0
  32. sandwitches/templates/components/ingredients_scripts.html +50 -0
  33. sandwitches/templates/components/ingredients_section.html +11 -0
  34. sandwitches/templates/components/instructions_section.html +9 -0
  35. sandwitches/templates/components/language_dialog.html +26 -0
  36. sandwitches/templates/components/navbar.html +27 -0
  37. sandwitches/templates/components/rating_section.html +66 -0
  38. sandwitches/templates/components/recipe_header.html +32 -0
  39. sandwitches/templates/components/search_form.html +106 -0
  40. sandwitches/templates/components/search_scripts.html +98 -0
  41. sandwitches/templates/components/side_menu.html +35 -0
  42. sandwitches/templates/components/user_menu.html +10 -0
  43. sandwitches/templates/detail.html +167 -110
  44. sandwitches/templates/favorites.html +42 -0
  45. sandwitches/templates/index.html +28 -61
  46. sandwitches/templates/partials/recipe_list.html +87 -0
  47. sandwitches/templates/recipe_form.html +119 -0
  48. sandwitches/templates/setup.html +1 -1
  49. sandwitches/templates/signup.html +114 -31
  50. sandwitches/templatetags/custom_filters.py +15 -0
  51. sandwitches/urls.py +56 -0
  52. sandwitches/utils.py +222 -0
  53. sandwitches/views.py +503 -14
  54. sandwitches-2.0.0.dist-info/METADATA +104 -0
  55. sandwitches-2.0.0.dist-info/RECORD +62 -0
  56. sandwitches/migrations/0002_historicalrecipe.py +0 -61
  57. sandwitches/migrations/0003_rating.py +0 -57
  58. sandwitches/migrations/0004_add_uploaded_by.py +0 -25
  59. sandwitches/migrations/0005_historicalrecipe_uploaded_by.py +0 -27
  60. sandwitches/migrations/0006_profile.py +0 -48
  61. sandwitches/migrations/0007_alter_rating_score.py +0 -23
  62. sandwitches/migrations/0008_delete_profile.py +0 -15
  63. sandwitches/templates/base_pico.html +0 -260
  64. sandwitches/templates/form.html +0 -16
  65. sandwitches-1.4.2.dist-info/METADATA +0 -25
  66. sandwitches-1.4.2.dist-info/RECORD +0 -35
  67. {sandwitches-1.4.2.dist-info → sandwitches-2.0.0.dist-info}/WHEEL +0 -0
@@ -1,13 +1,22 @@
1
- # Generated by Django 5.2.8 on 2025-11-05 19:15
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 get_user_model
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
- User = get_user_model()
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
- User,
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(User, related_name="ratings", on_delete=models.CASCADE)
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)) # ty:ignore[no-matching-overload]
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")) # ty:ignore[no-matching-overload]
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")) # ty:ignore[no-matching-overload]
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
- readable = p.is_file() and os.access(p, os.R_OK)
50
- if not readable:
51
- logging.error(f"Database file at {p} is not readable or does not exist.")
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
- writable = p.is_file() and os.access(p, os.W_OK)
77
- if not writable:
78
- logging.error(f"Database file at {p} is not writable or does not exist.")
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