sandwitches 2.4.0__tar.gz → 2.5.0__tar.gz

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 (89) hide show
  1. {sandwitches-2.4.0 → sandwitches-2.5.0}/PKG-INFO +1 -1
  2. {sandwitches-2.4.0 → sandwitches-2.5.0}/pyproject.toml +1 -1
  3. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/admin.py +10 -2
  4. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/forms.py +78 -13
  5. sandwitches-2.5.0/src/sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py +56 -0
  6. sandwitches-2.5.0/src/sandwitches/migrations/0016_user_theme.py +21 -0
  7. sandwitches-2.5.0/src/sandwitches/migrations/0017_setting_gotify_token_setting_gotify_url.py +31 -0
  8. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/models.py +47 -2
  9. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/tasks.py +31 -1
  10. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/admin_base.html +0 -3
  11. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/partials/order_rows.html +1 -1
  12. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/recipe_form.html +113 -18
  13. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/base.html +1 -1
  14. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/base_beer.html +0 -1
  15. sandwitches-2.5.0/src/sandwitches/templates/community.html +238 -0
  16. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/navbar.html +0 -6
  17. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/side_menu.html +4 -0
  18. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/user_menu.html +1 -0
  19. sandwitches-2.5.0/src/sandwitches/templates/order_detail.html +68 -0
  20. sandwitches-2.5.0/src/sandwitches/templates/profile.html +186 -0
  21. sandwitches-2.5.0/src/sandwitches/templates/settings.html +53 -0
  22. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/signup.html +0 -12
  23. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/urls.py +2 -0
  24. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/views.py +74 -1
  25. sandwitches-2.4.0/src/sandwitches/templates/community.html +0 -141
  26. sandwitches-2.4.0/src/sandwitches/templates/components/language_dialog.html +0 -26
  27. sandwitches-2.4.0/src/sandwitches/templates/profile.html +0 -95
  28. {sandwitches-2.4.0 → sandwitches-2.5.0}/README.md +0 -0
  29. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/__init__.py +0 -0
  30. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/api.py +0 -0
  31. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/asgi.py +0 -0
  32. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/feeds.py +0 -0
  33. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  34. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/locale/nl/LC_MESSAGES/django.po +0 -0
  35. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/management/__init__.py +0 -0
  36. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/management/commands/__init__.py +0 -0
  37. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/management/commands/reset_daily_orders.py +0 -0
  38. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0001_initial.py +0 -0
  39. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +0 -0
  40. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0003_setting.py +0 -0
  41. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +0 -0
  42. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0005_rating_comment.py +0 -0
  43. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +0 -0
  44. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py +0 -0
  45. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py +0 -0
  46. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py +0 -0
  47. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0010_rename_is_approved_historicalrecipe_is_community_made_and_more.py +0 -0
  48. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py +0 -0
  49. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0012_rename_is_community_made_historicalrecipe_is_approved_and_more.py +0 -0
  50. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0013_cartitem.py +0 -0
  51. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0014_ensure_groups_exist.py +0 -0
  52. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/__init__.py +0 -0
  53. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/settings.py +0 -0
  54. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/storage.py +0 -0
  55. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/confirm_delete.html +0 -0
  56. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/dashboard.html +0 -0
  57. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/order_list.html +0 -0
  58. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/partials/dashboard_charts.html +0 -0
  59. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/rating_list.html +0 -0
  60. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/recipe_approval_list.html +0 -0
  61. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/recipe_list.html +0 -0
  62. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/tag_form.html +0 -0
  63. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/tag_list.html +0 -0
  64. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/task_detail.html +0 -0
  65. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/task_list.html +0 -0
  66. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/user_form.html +0 -0
  67. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/user_list.html +0 -0
  68. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/cart.html +0 -0
  69. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/carousel_scripts.html +0 -0
  70. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/favorites_search_form.html +0 -0
  71. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/footer.html +0 -0
  72. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/ingredients_scripts.html +0 -0
  73. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/ingredients_section.html +0 -0
  74. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/instructions_section.html +0 -0
  75. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/rating_section.html +0 -0
  76. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/recipe_header.html +0 -0
  77. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/search_form.html +0 -0
  78. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/search_scripts.html +0 -0
  79. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/detail.html +0 -0
  80. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/favorites.html +0 -0
  81. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/index.html +0 -0
  82. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/login.html +0 -0
  83. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/partials/recipe_list.html +0 -0
  84. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/setup.html +0 -0
  85. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templatetags/__init__.py +0 -0
  86. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templatetags/custom_filters.py +0 -0
  87. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templatetags/markdown_extras.py +0 -0
  88. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/utils.py +0 -0
  89. {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sandwitches
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Add your description here
5
5
  Author: Martyn van Dijke
6
6
  Author-email: Martyn van Dijke <martijnvdijke600@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sandwitches"
3
- version = "2.4.0"
3
+ version = "2.5.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -84,7 +84,15 @@ class RatingAdmin(ImportExportModelAdmin):
84
84
  @admin.register(Order)
85
85
  class OrderAdmin(ImportExportModelAdmin):
86
86
  resource_classes = [OrderResource]
87
- list_display = ("id", "user", "recipe", "status", "total_price", "created_at")
88
- list_filter = ("status", "created_at")
87
+ list_display = (
88
+ "id",
89
+ "user",
90
+ "recipe",
91
+ "status",
92
+ "completed",
93
+ "total_price",
94
+ "created_at",
95
+ )
96
+ list_filter = ("status", "completed", "created_at")
89
97
  search_fields = ("user__username", "recipe__title")
90
98
  readonly_fields = ("total_price", "created_at", "updated_at")
@@ -1,5 +1,4 @@
1
1
  from django import forms
2
- from django.conf import settings
3
2
  from django.contrib.auth import get_user_model
4
3
  from django.contrib.auth.forms import UserCreationForm
5
4
  from django.utils.translation import gettext_lazy as _
@@ -47,11 +46,6 @@ class AdminSetupForm(forms.ModelForm, BaseUserFormMixin):
47
46
 
48
47
 
49
48
  class UserSignupForm(UserCreationForm, BaseUserFormMixin):
50
- language = forms.ChoiceField(
51
- choices=settings.LANGUAGES,
52
- label=_("Preferred language"),
53
- initial=settings.LANGUAGE_CODE,
54
- )
55
49
  avatar = forms.ImageField(label=_("Profile Image"), required=False)
56
50
  bio = forms.CharField(
57
51
  widget=forms.Textarea(attrs={"rows": 3}), label=_("Bio"), required=False
@@ -64,7 +58,6 @@ class UserSignupForm(UserCreationForm, BaseUserFormMixin):
64
58
  "first_name",
65
59
  "last_name",
66
60
  "email",
67
- "language",
68
61
  "avatar",
69
62
  "bio",
70
63
  )
@@ -77,7 +70,6 @@ class UserSignupForm(UserCreationForm, BaseUserFormMixin):
77
70
  user.is_superuser = False
78
71
  user.is_staff = False
79
72
  # Explicitly save the extra fields if they aren't automatically handled by ModelForm save (they should be if in Meta.fields)
80
- user.language = self.cleaned_data["language"]
81
73
  user.avatar = self.cleaned_data["avatar"]
82
74
  user.bio = self.cleaned_data["bio"]
83
75
  if commit:
@@ -86,6 +78,8 @@ class UserSignupForm(UserCreationForm, BaseUserFormMixin):
86
78
 
87
79
 
88
80
  class UserProfileForm(forms.ModelForm):
81
+ image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
82
+
89
83
  class Meta:
90
84
  model = User
91
85
  fields = (
@@ -96,8 +90,35 @@ class UserProfileForm(forms.ModelForm):
96
90
  "bio",
97
91
  )
98
92
 
93
+ def save(self, commit=True):
94
+ user = super().save(commit=False)
95
+ image_data = self.cleaned_data.get("image_data")
96
+ if image_data and image_data.startswith("data:image"):
97
+ import base64
98
+ from django.core.files.base import ContentFile
99
+
100
+ format, imgstr = image_data.split(";base64,")
101
+ ext = format.split("/")[-1]
102
+ data = ContentFile(base64.b64decode(imgstr), name=f"avatar.{ext}")
103
+ user.avatar = data
104
+ if commit:
105
+ user.save()
106
+ return user
107
+
108
+
109
+ class UserSettingsForm(forms.ModelForm):
110
+ class Meta:
111
+ model = User
112
+ fields = ("language", "theme")
113
+ labels = {
114
+ "language": _("Preferred Language"),
115
+ "theme": _("Preferred Theme"),
116
+ }
117
+
99
118
 
100
119
  class UserEditForm(forms.ModelForm):
120
+ image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
121
+
101
122
  class Meta:
102
123
  model = User
103
124
  fields = (
@@ -112,6 +133,21 @@ class UserEditForm(forms.ModelForm):
112
133
  "bio",
113
134
  )
114
135
 
136
+ def save(self, commit=True):
137
+ user = super().save(commit=False)
138
+ image_data = self.cleaned_data.get("image_data")
139
+ if image_data and image_data.startswith("data:image"):
140
+ import base64
141
+ from django.core.files.base import ContentFile
142
+
143
+ format, imgstr = image_data.split(";base64,")
144
+ ext = format.split("/")[-1]
145
+ data = ContentFile(base64.b64decode(imgstr), name=f"avatar.{ext}")
146
+ user.avatar = data
147
+ if commit:
148
+ user.save()
149
+ return user
150
+
115
151
 
116
152
  class TagForm(forms.ModelForm):
117
153
  class Meta:
@@ -126,6 +162,7 @@ class RecipeForm(forms.ModelForm):
126
162
  widget=forms.TextInput(attrs={"placeholder": _("e.g. spicy, vegan, quick")}),
127
163
  )
128
164
  rotation = forms.IntegerField(widget=forms.HiddenInput(), initial=0, required=False)
165
+ image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
129
166
 
130
167
  class Meta:
131
168
  model = Recipe
@@ -153,11 +190,22 @@ class RecipeForm(forms.ModelForm):
153
190
  )
154
191
 
155
192
  def save(self, commit=True):
156
- recipe = super().save(commit=commit)
193
+ recipe = super().save(commit=False)
194
+
195
+ # Handle base64 image data from cropper
196
+ image_data = self.cleaned_data.get("image_data")
197
+ if image_data and image_data.startswith("data:image"):
198
+ import base64
199
+ from django.core.files.base import ContentFile
200
+
201
+ format, imgstr = image_data.split(";base64,")
202
+ ext = format.split("/")[-1]
203
+ data = ContentFile(base64.b64decode(imgstr), name=f"recipe_image.{ext}")
204
+ recipe.image = data
157
205
 
158
- # Handle rotation if an image exists and rotation is requested
206
+ # Handle rotation if an image exists and rotation is requested (fallback for simple rotation)
159
207
  rotation = self.cleaned_data.get("rotation", 0)
160
- if rotation != 0 and recipe.image:
208
+ if rotation != 0 and recipe.image and not image_data:
161
209
  try:
162
210
  from PIL import Image as PILImage
163
211
 
@@ -169,9 +217,9 @@ class RecipeForm(forms.ModelForm):
169
217
  print(f"Error rotating image: {e}")
170
218
 
171
219
  if commit:
220
+ recipe.save()
172
221
  recipe.set_tags_from_string(self.cleaned_data.get("tags_string", ""))
173
222
  else:
174
- # We'll need to handle this in the view if commit=False
175
223
  self.save_m2m = lambda: recipe.set_tags_from_string(
176
224
  self.cleaned_data.get("tags_string", "")
177
225
  )
@@ -184,6 +232,7 @@ class UserRecipeSubmissionForm(forms.ModelForm):
184
232
  label=_("Tags (comma separated)"),
185
233
  widget=forms.TextInput(attrs={"placeholder": _("e.g. spicy, vegan, quick")}),
186
234
  )
235
+ image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
187
236
 
188
237
  class Meta:
189
238
  model = Recipe
@@ -201,8 +250,21 @@ class UserRecipeSubmissionForm(forms.ModelForm):
201
250
  }
202
251
 
203
252
  def save(self, commit=True):
204
- recipe = super().save(commit=commit)
253
+ recipe = super().save(commit=False)
254
+
255
+ # Handle base64 image data from cropper
256
+ image_data = self.cleaned_data.get("image_data")
257
+ if image_data and image_data.startswith("data:image"):
258
+ import base64
259
+ from django.core.files.base import ContentFile
260
+
261
+ format, imgstr = image_data.split(";base64,")
262
+ ext = format.split("/")[-1]
263
+ data = ContentFile(base64.b64decode(imgstr), name=f"recipe_image.{ext}")
264
+ recipe.image = data
265
+
205
266
  if commit:
267
+ recipe.save()
206
268
  recipe.set_tags_from_string(self.cleaned_data.get("tags_string", ""))
207
269
  else:
208
270
  self.save_m2m = lambda: recipe.set_tags_from_string(
@@ -239,7 +301,10 @@ class SettingForm(forms.ModelForm):
239
301
  "ai_connection_point",
240
302
  "ai_model",
241
303
  "ai_api_key",
304
+ "gotify_url",
305
+ "gotify_token",
242
306
  ]
243
307
  widgets = {
244
308
  "ai_api_key": forms.PasswordInput(render_value=True),
309
+ "gotify_token": forms.PasswordInput(render_value=True),
245
310
  }
@@ -0,0 +1,56 @@
1
+ # Generated by Django 6.0.1 on 2026-01-27 08:33
2
+
3
+ import django.core.validators
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("sandwitches", "0014_ensure_groups_exist"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="order",
15
+ name="completed",
16
+ field=models.BooleanField(default=False),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name="order",
20
+ name="status",
21
+ field=models.CharField(
22
+ choices=[
23
+ ("PENDING", "Pending"),
24
+ ("PREPARING", "Preparing"),
25
+ ("MADE", "Made"),
26
+ ("SHIPPED", "Shipped"),
27
+ ("COMPLETED", "Completed"),
28
+ ("CANCELLED", "Cancelled"),
29
+ ],
30
+ default="PENDING",
31
+ max_length=20,
32
+ ),
33
+ ),
34
+ migrations.AlterField(
35
+ model_name="setting",
36
+ name="email",
37
+ field=models.EmailField(
38
+ blank=True,
39
+ max_length=254,
40
+ null=True,
41
+ validators=[django.core.validators.EmailValidator()],
42
+ ),
43
+ ),
44
+ migrations.AlterField(
45
+ model_name="user",
46
+ name="email",
47
+ field=models.EmailField(
48
+ max_length=254, validators=[django.core.validators.EmailValidator()]
49
+ ),
50
+ ),
51
+ migrations.AlterField(
52
+ model_name="user",
53
+ name="username",
54
+ field=models.CharField(max_length=150, unique=True),
55
+ ),
56
+ ]
@@ -0,0 +1,21 @@
1
+ # Generated by Django 6.0.1 on 2026-01-28 09:08
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("sandwitches", "0015_order_completed_alter_order_status_and_more"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="user",
14
+ name="theme",
15
+ field=models.CharField(
16
+ choices=[("light", "Light"), ("dark", "Dark")],
17
+ default="light",
18
+ max_length=10,
19
+ ),
20
+ ),
21
+ ]
@@ -0,0 +1,31 @@
1
+ # Generated by Django 6.0.1 on 2026-01-28 12:45
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("sandwitches", "0016_user_theme"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="setting",
14
+ name="gotify_token",
15
+ field=models.CharField(
16
+ blank=True,
17
+ help_text="The application token for Gotify",
18
+ max_length=255,
19
+ null=True,
20
+ ),
21
+ ),
22
+ migrations.AddField(
23
+ model_name="setting",
24
+ name="gotify_url",
25
+ field=models.URLField(
26
+ blank=True,
27
+ help_text="The URL of your Gotify server (e.g., https://gotify.example.com)",
28
+ null=True,
29
+ ),
30
+ ),
31
+ ]
@@ -4,13 +4,14 @@ from .storage import HashedFilenameStorage
4
4
  from simple_history.models import HistoricalRecords
5
5
  from django.contrib.auth.models import AbstractUser
6
6
  from django.db.models import Avg
7
- from .tasks import email_users, notify_order_submitted
7
+ from .tasks import email_users, notify_order_submitted, send_gotify_notification
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
12
  from solo.models import SingletonModel
13
13
  from django.core.exceptions import ValidationError
14
+ from django.core.validators import EmailValidator
14
15
 
15
16
  from imagekit.models import ImageSpecField
16
17
  from imagekit.processors import ResizeToFill
@@ -21,11 +22,23 @@ hashed_storage = HashedFilenameStorage()
21
22
  class Setting(SingletonModel):
22
23
  site_name = models.CharField(max_length=255, default="Sandwitches")
23
24
  site_description = models.TextField(blank=True, null=True)
24
- email = models.EmailField(blank=True, null=True)
25
+ email = models.EmailField(blank=True, null=True, validators=[EmailValidator()])
25
26
  ai_connection_point = models.URLField(blank=True, null=True)
26
27
  ai_model = models.CharField(max_length=255, blank=True, null=True)
27
28
  ai_api_key = models.CharField(max_length=255, blank=True, null=True)
28
29
 
30
+ gotify_url = models.URLField(
31
+ blank=True,
32
+ null=True,
33
+ help_text="The URL of your Gotify server (e.g., https://gotify.example.com)",
34
+ )
35
+ gotify_token = models.CharField(
36
+ max_length=255,
37
+ blank=True,
38
+ null=True,
39
+ help_text="The application token for Gotify",
40
+ )
41
+
29
42
  def __str__(self):
30
43
  return "Site Settings"
31
44
 
@@ -34,6 +47,8 @@ class Setting(SingletonModel):
34
47
 
35
48
 
36
49
  class User(AbstractUser):
50
+ username = models.CharField(max_length=150, unique=True)
51
+ email = models.EmailField(validators=[EmailValidator()])
37
52
  avatar = models.ImageField(upload_to="avatars", blank=True, null=True)
38
53
  avatar_thumbnail = ImageSpecField(
39
54
  source="avatar",
@@ -47,6 +62,11 @@ class User(AbstractUser):
47
62
  choices=settings.LANGUAGES,
48
63
  default=settings.LANGUAGE_CODE,
49
64
  )
65
+ theme = models.CharField(
66
+ max_length=10,
67
+ choices=[("light", "Light"), ("dark", "Dark")],
68
+ default="light",
69
+ )
50
70
  favorites = models.ManyToManyField(
51
71
  "Recipe", related_name="favorited_by", blank=True
52
72
  )
@@ -58,6 +78,16 @@ class User(AbstractUser):
58
78
  def __str__(self):
59
79
  return self.username
60
80
 
81
+ def save(self, *args, **kwargs):
82
+ is_new = self.pk is None
83
+ super().save(*args, **kwargs)
84
+ if is_new:
85
+ send_gotify_notification.enqueue(
86
+ title="New User Created",
87
+ message=f"User {self.username} has joined Sandwitches!",
88
+ priority=4,
89
+ )
90
+
61
91
 
62
92
  class Tag(models.Model):
63
93
  name = models.CharField(max_length=50, unique=True)
@@ -170,6 +200,12 @@ class Recipe(models.Model):
170
200
  logging.warning(
171
201
  "Email sending is disabled; not sending email notification, make sure SEND_EMAIL is set to True in settings."
172
202
  )
203
+
204
+ send_gotify_notification.enqueue(
205
+ title="New Recipe Uploaded",
206
+ message=f"A new recipe '{self.title}' has been uploaded by {self.uploaded_by or 'Unknown'}.",
207
+ priority=5,
208
+ )
173
209
  else:
174
210
  logging.debug(
175
211
  "Existing recipe saved (update); skipping email notification."
@@ -230,6 +266,9 @@ class Rating(models.Model):
230
266
  class Order(models.Model):
231
267
  STATUS_CHOICES = (
232
268
  ("PENDING", "Pending"),
269
+ ("PREPARING", "Preparing"),
270
+ ("MADE", "Made"),
271
+ ("SHIPPED", "Shipped"),
233
272
  ("COMPLETED", "Completed"),
234
273
  ("CANCELLED", "Cancelled"),
235
274
  )
@@ -239,6 +278,7 @@ class Order(models.Model):
239
278
  )
240
279
  recipe = models.ForeignKey(Recipe, related_name="orders", on_delete=models.CASCADE)
241
280
  status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
281
+ completed = models.BooleanField(default=False)
242
282
  total_price = models.DecimalField(max_digits=6, decimal_places=2)
243
283
  created_at = models.DateTimeField(auto_now_add=True)
244
284
  updated_at = models.DateTimeField(auto_now=True)
@@ -275,6 +315,11 @@ class Order(models.Model):
275
315
 
276
316
  if is_new:
277
317
  notify_order_submitted.enqueue(order_id=self.pk)
318
+ send_gotify_notification.enqueue(
319
+ title="New Order Received",
320
+ message=f"Order #{self.pk} for '{self.recipe.title}' by {self.user.username}. Total: {self.total_price}€",
321
+ priority=6,
322
+ )
278
323
 
279
324
  def __str__(self):
280
325
  return f"Order #{self.pk} - {self.user} - {self.recipe}"
@@ -1,5 +1,5 @@
1
- # from gunicorn.http.wsgi import log
2
1
  import logging
2
+ import requests
3
3
 
4
4
  # from django.core.mail import send_mail
5
5
  from django_tasks import task
@@ -109,6 +109,36 @@ def notify_order_submitted(order_id):
109
109
  logging.info(f"Order confirmation email sent to {user.email} for order {order.id}")
110
110
 
111
111
 
112
+ @task(priority=1, queue_name="emails")
113
+ def send_gotify_notification(title, message, priority=5):
114
+ from .models import Setting
115
+
116
+ config = Setting.get_solo()
117
+ url = config.gotify_url
118
+ token = config.gotify_token
119
+
120
+ if not url or not token:
121
+ logging.debug("Gotify URL or Token not configured. Skipping notification.")
122
+ return False
123
+
124
+ try:
125
+ response = requests.post(
126
+ f"{url.rstrip('/')}/message?token={token}",
127
+ json={
128
+ "title": title,
129
+ "message": message,
130
+ "priority": priority,
131
+ },
132
+ timeout=10,
133
+ )
134
+ response.raise_for_status()
135
+ logging.info(f"Gotify notification sent: {title}")
136
+ return True
137
+ except Exception as e:
138
+ logging.error(f"Failed to send Gotify notification: {e}")
139
+ return False
140
+
141
+
112
142
  def send_emails(recipe_id, emails):
113
143
  from .models import Recipe
114
144
 
@@ -30,9 +30,6 @@
30
30
  <h6 class="max">{% block admin_title %}{% trans "Sandwitches Admin" %}{% endblock %}</h6>
31
31
  </a>
32
32
  <div class="max"></div>
33
- <button class="circle transparent" onclick="toggleMode()">
34
- <i>dark_mode</i>
35
- </button>
36
33
 
37
34
  {% if user.avatar %}
38
35
  <img src="{{ user.avatar.url }}" class="circle" data-ui="#user-menu">
@@ -15,7 +15,7 @@
15
15
  <td>{{ order.recipe.title }}</td>
16
16
  <td>{{ order.total_price }} €</td>
17
17
  <td>
18
- <span class="chip {% if order.status == 'PENDING' %}surface-variant{% elif order.status == 'COMPLETED' %}primary{% else %}error{% endif %}">
18
+ <span class="chip {% if order.status == 'PENDING' %}surface-variant{% elif order.status == 'COMPLETED' %}primary{% elif order.status == 'CANCELLED' %}error{% else %}secondary{% endif %}">
19
19
  {{ order.get_status_display }}
20
20
  </span>
21
21
  </td>