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.
- {sandwitches-2.4.0 → sandwitches-2.5.0}/PKG-INFO +1 -1
- {sandwitches-2.4.0 → sandwitches-2.5.0}/pyproject.toml +1 -1
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/admin.py +10 -2
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/forms.py +78 -13
- sandwitches-2.5.0/src/sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py +56 -0
- sandwitches-2.5.0/src/sandwitches/migrations/0016_user_theme.py +21 -0
- sandwitches-2.5.0/src/sandwitches/migrations/0017_setting_gotify_token_setting_gotify_url.py +31 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/models.py +47 -2
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/tasks.py +31 -1
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/admin_base.html +0 -3
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/partials/order_rows.html +1 -1
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/recipe_form.html +113 -18
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/base.html +1 -1
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/base_beer.html +0 -1
- sandwitches-2.5.0/src/sandwitches/templates/community.html +238 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/navbar.html +0 -6
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/side_menu.html +4 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/user_menu.html +1 -0
- sandwitches-2.5.0/src/sandwitches/templates/order_detail.html +68 -0
- sandwitches-2.5.0/src/sandwitches/templates/profile.html +186 -0
- sandwitches-2.5.0/src/sandwitches/templates/settings.html +53 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/signup.html +0 -12
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/urls.py +2 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/views.py +74 -1
- sandwitches-2.4.0/src/sandwitches/templates/community.html +0 -141
- sandwitches-2.4.0/src/sandwitches/templates/components/language_dialog.html +0 -26
- sandwitches-2.4.0/src/sandwitches/templates/profile.html +0 -95
- {sandwitches-2.4.0 → sandwitches-2.5.0}/README.md +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/__init__.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/api.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/asgi.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/feeds.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/locale/nl/LC_MESSAGES/django.po +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/management/__init__.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/management/commands/__init__.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/management/commands/reset_daily_orders.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0001_initial.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0003_setting.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0005_rating_comment.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0010_rename_is_approved_historicalrecipe_is_community_made_and_more.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0012_rename_is_community_made_historicalrecipe_is_approved_and_more.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0013_cartitem.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/0014_ensure_groups_exist.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/migrations/__init__.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/settings.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/storage.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/confirm_delete.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/dashboard.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/order_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/partials/dashboard_charts.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/rating_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/recipe_approval_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/recipe_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/tag_form.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/tag_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/task_detail.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/task_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/user_form.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/user_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/cart.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/carousel_scripts.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/favorites_search_form.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/footer.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/ingredients_scripts.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/ingredients_section.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/instructions_section.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/rating_section.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/recipe_header.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/search_form.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/components/search_scripts.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/detail.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/favorites.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/index.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/login.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/partials/recipe_list.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/setup.html +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templatetags/__init__.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templatetags/custom_filters.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templatetags/markdown_extras.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/utils.py +0 -0
- {sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/wsgi.py +0 -0
|
@@ -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 = (
|
|
88
|
-
|
|
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=
|
|
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=
|
|
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
|
}
|
sandwitches-2.5.0/src/sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py
ADDED
|
@@ -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">
|
{sandwitches-2.4.0 → sandwitches-2.5.0}/src/sandwitches/templates/admin/partials/order_rows.html
RENAMED
|
@@ -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{%
|
|
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>
|