sandwitches 2.4.0__py3-none-any.whl → 2.4.1__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/admin.py +10 -2
- sandwitches/forms.py +65 -5
- sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py +56 -0
- sandwitches/models.py +8 -1
- sandwitches/templates/admin/partials/order_rows.html +1 -1
- sandwitches/templates/admin/recipe_form.html +113 -18
- sandwitches/templates/community.html +111 -14
- sandwitches/templates/order_detail.html +68 -0
- sandwitches/templates/profile.html +90 -0
- sandwitches/urls.py +1 -0
- sandwitches/views.py +46 -1
- {sandwitches-2.4.0.dist-info → sandwitches-2.4.1.dist-info}/METADATA +1 -1
- {sandwitches-2.4.0.dist-info → sandwitches-2.4.1.dist-info}/RECORD +14 -12
- {sandwitches-2.4.0.dist-info → sandwitches-2.4.1.dist-info}/WHEEL +0 -0
sandwitches/admin.py
CHANGED
|
@@ -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")
|
sandwitches/forms.py
CHANGED
|
@@ -86,6 +86,8 @@ class UserSignupForm(UserCreationForm, BaseUserFormMixin):
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
class UserProfileForm(forms.ModelForm):
|
|
89
|
+
image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
90
|
+
|
|
89
91
|
class Meta:
|
|
90
92
|
model = User
|
|
91
93
|
fields = (
|
|
@@ -96,8 +98,25 @@ class UserProfileForm(forms.ModelForm):
|
|
|
96
98
|
"bio",
|
|
97
99
|
)
|
|
98
100
|
|
|
101
|
+
def save(self, commit=True):
|
|
102
|
+
user = super().save(commit=False)
|
|
103
|
+
image_data = self.cleaned_data.get("image_data")
|
|
104
|
+
if image_data and image_data.startswith("data:image"):
|
|
105
|
+
import base64
|
|
106
|
+
from django.core.files.base import ContentFile
|
|
107
|
+
|
|
108
|
+
format, imgstr = image_data.split(";base64,")
|
|
109
|
+
ext = format.split("/")[-1]
|
|
110
|
+
data = ContentFile(base64.b64decode(imgstr), name=f"avatar.{ext}")
|
|
111
|
+
user.avatar = data
|
|
112
|
+
if commit:
|
|
113
|
+
user.save()
|
|
114
|
+
return user
|
|
115
|
+
|
|
99
116
|
|
|
100
117
|
class UserEditForm(forms.ModelForm):
|
|
118
|
+
image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
119
|
+
|
|
101
120
|
class Meta:
|
|
102
121
|
model = User
|
|
103
122
|
fields = (
|
|
@@ -112,6 +131,21 @@ class UserEditForm(forms.ModelForm):
|
|
|
112
131
|
"bio",
|
|
113
132
|
)
|
|
114
133
|
|
|
134
|
+
def save(self, commit=True):
|
|
135
|
+
user = super().save(commit=False)
|
|
136
|
+
image_data = self.cleaned_data.get("image_data")
|
|
137
|
+
if image_data and image_data.startswith("data:image"):
|
|
138
|
+
import base64
|
|
139
|
+
from django.core.files.base import ContentFile
|
|
140
|
+
|
|
141
|
+
format, imgstr = image_data.split(";base64,")
|
|
142
|
+
ext = format.split("/")[-1]
|
|
143
|
+
data = ContentFile(base64.b64decode(imgstr), name=f"avatar.{ext}")
|
|
144
|
+
user.avatar = data
|
|
145
|
+
if commit:
|
|
146
|
+
user.save()
|
|
147
|
+
return user
|
|
148
|
+
|
|
115
149
|
|
|
116
150
|
class TagForm(forms.ModelForm):
|
|
117
151
|
class Meta:
|
|
@@ -126,6 +160,7 @@ class RecipeForm(forms.ModelForm):
|
|
|
126
160
|
widget=forms.TextInput(attrs={"placeholder": _("e.g. spicy, vegan, quick")}),
|
|
127
161
|
)
|
|
128
162
|
rotation = forms.IntegerField(widget=forms.HiddenInput(), initial=0, required=False)
|
|
163
|
+
image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
129
164
|
|
|
130
165
|
class Meta:
|
|
131
166
|
model = Recipe
|
|
@@ -153,11 +188,22 @@ class RecipeForm(forms.ModelForm):
|
|
|
153
188
|
)
|
|
154
189
|
|
|
155
190
|
def save(self, commit=True):
|
|
156
|
-
recipe = super().save(commit=
|
|
191
|
+
recipe = super().save(commit=False)
|
|
192
|
+
|
|
193
|
+
# Handle base64 image data from cropper
|
|
194
|
+
image_data = self.cleaned_data.get("image_data")
|
|
195
|
+
if image_data and image_data.startswith("data:image"):
|
|
196
|
+
import base64
|
|
197
|
+
from django.core.files.base import ContentFile
|
|
157
198
|
|
|
158
|
-
|
|
199
|
+
format, imgstr = image_data.split(";base64,")
|
|
200
|
+
ext = format.split("/")[-1]
|
|
201
|
+
data = ContentFile(base64.b64decode(imgstr), name=f"recipe_image.{ext}")
|
|
202
|
+
recipe.image = data
|
|
203
|
+
|
|
204
|
+
# Handle rotation if an image exists and rotation is requested (fallback for simple rotation)
|
|
159
205
|
rotation = self.cleaned_data.get("rotation", 0)
|
|
160
|
-
if rotation != 0 and recipe.image:
|
|
206
|
+
if rotation != 0 and recipe.image and not image_data:
|
|
161
207
|
try:
|
|
162
208
|
from PIL import Image as PILImage
|
|
163
209
|
|
|
@@ -169,9 +215,9 @@ class RecipeForm(forms.ModelForm):
|
|
|
169
215
|
print(f"Error rotating image: {e}")
|
|
170
216
|
|
|
171
217
|
if commit:
|
|
218
|
+
recipe.save()
|
|
172
219
|
recipe.set_tags_from_string(self.cleaned_data.get("tags_string", ""))
|
|
173
220
|
else:
|
|
174
|
-
# We'll need to handle this in the view if commit=False
|
|
175
221
|
self.save_m2m = lambda: recipe.set_tags_from_string(
|
|
176
222
|
self.cleaned_data.get("tags_string", "")
|
|
177
223
|
)
|
|
@@ -184,6 +230,7 @@ class UserRecipeSubmissionForm(forms.ModelForm):
|
|
|
184
230
|
label=_("Tags (comma separated)"),
|
|
185
231
|
widget=forms.TextInput(attrs={"placeholder": _("e.g. spicy, vegan, quick")}),
|
|
186
232
|
)
|
|
233
|
+
image_data = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
187
234
|
|
|
188
235
|
class Meta:
|
|
189
236
|
model = Recipe
|
|
@@ -201,8 +248,21 @@ class UserRecipeSubmissionForm(forms.ModelForm):
|
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
def save(self, commit=True):
|
|
204
|
-
recipe = super().save(commit=
|
|
251
|
+
recipe = super().save(commit=False)
|
|
252
|
+
|
|
253
|
+
# Handle base64 image data from cropper
|
|
254
|
+
image_data = self.cleaned_data.get("image_data")
|
|
255
|
+
if image_data and image_data.startswith("data:image"):
|
|
256
|
+
import base64
|
|
257
|
+
from django.core.files.base import ContentFile
|
|
258
|
+
|
|
259
|
+
format, imgstr = image_data.split(";base64,")
|
|
260
|
+
ext = format.split("/")[-1]
|
|
261
|
+
data = ContentFile(base64.b64decode(imgstr), name=f"recipe_image.{ext}")
|
|
262
|
+
recipe.image = data
|
|
263
|
+
|
|
205
264
|
if commit:
|
|
265
|
+
recipe.save()
|
|
206
266
|
recipe.set_tags_from_string(self.cleaned_data.get("tags_string", ""))
|
|
207
267
|
else:
|
|
208
268
|
self.save_m2m = lambda: recipe.set_tags_from_string(
|
|
@@ -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
|
+
]
|
sandwitches/models.py
CHANGED
|
@@ -11,6 +11,7 @@ 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,7 +22,7 @@ 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)
|
|
@@ -34,6 +35,8 @@ class Setting(SingletonModel):
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class User(AbstractUser):
|
|
38
|
+
username = models.CharField(max_length=150, unique=True)
|
|
39
|
+
email = models.EmailField(validators=[EmailValidator()])
|
|
37
40
|
avatar = models.ImageField(upload_to="avatars", blank=True, null=True)
|
|
38
41
|
avatar_thumbnail = ImageSpecField(
|
|
39
42
|
source="avatar",
|
|
@@ -230,6 +233,9 @@ class Rating(models.Model):
|
|
|
230
233
|
class Order(models.Model):
|
|
231
234
|
STATUS_CHOICES = (
|
|
232
235
|
("PENDING", "Pending"),
|
|
236
|
+
("PREPARING", "Preparing"),
|
|
237
|
+
("MADE", "Made"),
|
|
238
|
+
("SHIPPED", "Shipped"),
|
|
233
239
|
("COMPLETED", "Completed"),
|
|
234
240
|
("CANCELLED", "Cancelled"),
|
|
235
241
|
)
|
|
@@ -239,6 +245,7 @@ class Order(models.Model):
|
|
|
239
245
|
)
|
|
240
246
|
recipe = models.ForeignKey(Recipe, related_name="orders", on_delete=models.CASCADE)
|
|
241
247
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
|
|
248
|
+
completed = models.BooleanField(default=False)
|
|
242
249
|
total_price = models.DecimalField(max_digits=6, decimal_places=2)
|
|
243
250
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
244
251
|
updated_at = models.DateTimeField(auto_now=True)
|
|
@@ -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>
|
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
.form-section {
|
|
32
32
|
margin-bottom: 2rem;
|
|
33
33
|
}
|
|
34
|
+
/* Cropper styles */
|
|
35
|
+
.cropper-container {
|
|
36
|
+
max-height: 70vh;
|
|
37
|
+
}
|
|
38
|
+
#cropper-image {
|
|
39
|
+
display: block;
|
|
40
|
+
max-width: 100%;
|
|
41
|
+
}
|
|
34
42
|
</style>
|
|
35
43
|
{% endblock %}
|
|
36
44
|
|
|
@@ -38,6 +46,7 @@
|
|
|
38
46
|
<form method="post" enctype="multipart/form-data" id="recipe-form">
|
|
39
47
|
{% csrf_token %}
|
|
40
48
|
{{ form.rotation }}
|
|
49
|
+
{{ form.image_data }}
|
|
41
50
|
<div class="grid">
|
|
42
51
|
<!-- Top Section: Title, Tags, and Image -->
|
|
43
52
|
<div class="s12 m8">
|
|
@@ -107,25 +116,21 @@
|
|
|
107
116
|
|
|
108
117
|
<div class="relative mb-1" style="overflow: hidden; min-height: 200px; display: flex; align-items: center; justify-content: center;">
|
|
109
118
|
{% if recipe.image %}
|
|
110
|
-
<img src="{{ recipe.image_medium.url }}?v={% now "U" %}" class="responsive round" id="image-preview" style="max-height: 300px; width: 100%; object-fit: contain;
|
|
119
|
+
<img src="{{ recipe.image_medium.url }}?v={% now "U" %}" class="responsive round" id="image-preview" style="max-height: 300px; width: 100%; object-fit: contain;">
|
|
111
120
|
{% else %}
|
|
121
|
+
<img src="" class="responsive round" id="image-preview" style="max-height: 300px; width: 100%; object-fit: contain; display: none;">
|
|
112
122
|
<div class="medium-height middle-align center-align gray1 round" id="image-placeholder" style="width: 100%;">
|
|
113
123
|
<i class="extra">image</i>
|
|
114
124
|
</div>
|
|
115
125
|
{% endif %}
|
|
116
126
|
</div>
|
|
117
127
|
|
|
118
|
-
{% if recipe.image %}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
</button>
|
|
123
|
-
<div class="divider vertical"></div>
|
|
124
|
-
<button type="button" class="button transparent max" onclick="rotatePreview(90)" title="{% trans 'Rotate 90° CW' %}">
|
|
125
|
-
<i>rotate_right</i>
|
|
128
|
+
<div id="image-tools" class="row no-space border round mb-1" style="{% if not recipe.image %}display: none;{% endif %}">
|
|
129
|
+
<button type="button" class="button transparent max" onclick="openCropper()" title="{% trans 'Edit Image' %}">
|
|
130
|
+
<i>crop_rotate</i>
|
|
131
|
+
<span>{% trans "Edit" %}</span>
|
|
126
132
|
</button>
|
|
127
133
|
</div>
|
|
128
|
-
{% endif %}
|
|
129
134
|
|
|
130
135
|
<div class="field file border round">
|
|
131
136
|
<input type="text" readonly>
|
|
@@ -174,21 +179,111 @@
|
|
|
174
179
|
</button>
|
|
175
180
|
</nav>
|
|
176
181
|
</form>
|
|
182
|
+
|
|
183
|
+
<dialog id="cropper-dialog" class="large">
|
|
184
|
+
<div class="padding">
|
|
185
|
+
<h5 class="bold mb-1">{% trans "Edit Image" %}</h5>
|
|
186
|
+
<div class="cropper-container mb-1">
|
|
187
|
+
<img id="cropper-image" src="">
|
|
188
|
+
</div>
|
|
189
|
+
<div class="row scroll no-space border round mb-1">
|
|
190
|
+
<button type="button" class="button transparent max" onclick="cropper.rotate(-90)" title="{% trans 'Rotate Left' %}">
|
|
191
|
+
<i>rotate_left</i>
|
|
192
|
+
</button>
|
|
193
|
+
<button type="button" class="button transparent max" onclick="cropper.rotate(90)" title="{% trans 'Rotate Right' %}">
|
|
194
|
+
<i>rotate_right</i>
|
|
195
|
+
</button>
|
|
196
|
+
<div class="divider vertical"></div>
|
|
197
|
+
<button type="button" class="button transparent max" onclick="cropper.scaleX(-cropper.getData().scaleX || -1)" title="{% trans 'Flip Horizontal' %}">
|
|
198
|
+
<i>flip</i>
|
|
199
|
+
</button>
|
|
200
|
+
<button type="button" class="button transparent max" onclick="cropper.scaleY(-cropper.getData().scaleY || -1)" title="{% trans 'Flip Vertical' %}">
|
|
201
|
+
<i>flip</i>
|
|
202
|
+
</button>
|
|
203
|
+
<div class="divider vertical"></div>
|
|
204
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(1)" title="{% trans '1:1' %}">1:1</button>
|
|
205
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(4/3)" title="{% trans '4:3' %}">4:3</button>
|
|
206
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(16/9)" title="{% trans '16:9' %}">16:9</button>
|
|
207
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(NaN)" title="{% trans 'Free' %}">
|
|
208
|
+
<i>crop_free</i>
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
<nav class="right-align">
|
|
212
|
+
<button type="button" class="button transparent" onclick="ui('#cropper-dialog')">{% trans "Cancel" %}</button>
|
|
213
|
+
<button type="button" class="button primary" onclick="applyCrop()">{% trans "Apply" %}</button>
|
|
214
|
+
</nav>
|
|
215
|
+
</div>
|
|
216
|
+
</dialog>
|
|
177
217
|
{% endblock %}
|
|
178
218
|
|
|
179
219
|
{% block admin_scripts %}
|
|
180
220
|
<script>
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
221
|
+
let cropper;
|
|
222
|
+
const imagePreview = document.getElementById('image-preview');
|
|
223
|
+
const imagePlaceholder = document.getElementById('image-placeholder');
|
|
224
|
+
const imageTools = document.getElementById('image-tools');
|
|
225
|
+
const imageInput = document.getElementById('id_image');
|
|
226
|
+
const imageDataInput = document.getElementById('id_image_data');
|
|
227
|
+
const cropperImage = document.getElementById('cropper-image');
|
|
228
|
+
|
|
229
|
+
function openCropper() {
|
|
230
|
+
if (!imagePreview.src || imagePreview.src === window.location.href) return;
|
|
231
|
+
cropperImage.src = imagePreview.src;
|
|
232
|
+
ui('#cropper-dialog');
|
|
233
|
+
|
|
234
|
+
if (cropper) {
|
|
235
|
+
cropper.destroy();
|
|
189
236
|
}
|
|
237
|
+
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
cropper = new Cropper(cropperImage, {
|
|
240
|
+
viewMode: 1,
|
|
241
|
+
autoCropArea: 1,
|
|
242
|
+
responsive: true,
|
|
243
|
+
restore: false,
|
|
244
|
+
checkCrossOrigin: true,
|
|
245
|
+
guides: true,
|
|
246
|
+
center: true,
|
|
247
|
+
highlight: false,
|
|
248
|
+
cropBoxMovable: true,
|
|
249
|
+
cropBoxResizable: true,
|
|
250
|
+
toggleDragModeOnDblclick: false,
|
|
251
|
+
});
|
|
252
|
+
}, 100);
|
|
190
253
|
}
|
|
191
254
|
|
|
255
|
+
function applyCrop() {
|
|
256
|
+
const canvas = cropper.getCroppedCanvas({
|
|
257
|
+
maxWidth: 2000,
|
|
258
|
+
maxHeight: 2000,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const croppedData = canvas.toDataURL('image/jpeg', 0.9);
|
|
262
|
+
imagePreview.src = croppedData;
|
|
263
|
+
imagePreview.style.display = 'block';
|
|
264
|
+
if (imagePlaceholder) imagePlaceholder.style.display = 'none';
|
|
265
|
+
imageTools.style.display = 'flex';
|
|
266
|
+
imageDataInput.value = croppedData;
|
|
267
|
+
|
|
268
|
+
ui('#cropper-dialog');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
imageInput.addEventListener('change', function(e) {
|
|
272
|
+
const files = e.target.files;
|
|
273
|
+
if (files && files.length > 0) {
|
|
274
|
+
const reader = new FileReader();
|
|
275
|
+
reader.onload = function(event) {
|
|
276
|
+
imagePreview.src = event.target.result;
|
|
277
|
+
imagePreview.style.display = 'block';
|
|
278
|
+
if (imagePlaceholder) imagePlaceholder.style.display = 'none';
|
|
279
|
+
imageTools.style.display = 'flex';
|
|
280
|
+
// Automatically open cropper for new images
|
|
281
|
+
openCropper();
|
|
282
|
+
};
|
|
283
|
+
reader.readAsDataURL(files[0]);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
192
287
|
document.addEventListener('DOMContentLoaded', function() {
|
|
193
288
|
const fields = ['id_description', 'id_ingredients', 'id_instructions'];
|
|
194
289
|
fields.forEach(id => {
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
{% block title %}{% trans "Community" %}{% endblock %}
|
|
5
5
|
|
|
6
6
|
{% block content %}
|
|
7
|
+
<style>
|
|
8
|
+
.cropper-container {
|
|
9
|
+
max-height: 70vh;
|
|
10
|
+
}
|
|
11
|
+
#cropper-image {
|
|
12
|
+
display: block;
|
|
13
|
+
max-width: 100%;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
7
16
|
<main class="responsive">
|
|
8
17
|
<div class="large-space"></div>
|
|
9
18
|
<article class="round s12 m10 l8 offset-m1 offset-l2 elevate">
|
|
@@ -15,6 +24,7 @@
|
|
|
15
24
|
|
|
16
25
|
<form method="post" enctype="multipart/form-data">
|
|
17
26
|
{% csrf_token %}
|
|
27
|
+
{{ form.image_data }}
|
|
18
28
|
|
|
19
29
|
<div class="grid">
|
|
20
30
|
<div class="s12">
|
|
@@ -53,15 +63,21 @@
|
|
|
53
63
|
|
|
54
64
|
<!-- Image Upload with Preview -->
|
|
55
65
|
<div class="s12 m6">
|
|
56
|
-
<div class="field label border round">
|
|
66
|
+
<div class="field file label border round">
|
|
67
|
+
<input type="text" readonly>
|
|
57
68
|
{{ form.image }}
|
|
58
69
|
<label>{% trans "Image" %}</label>
|
|
70
|
+
<i>publish</i>
|
|
59
71
|
</div>
|
|
60
72
|
</div>
|
|
61
73
|
<div class="s12 m6 center-align relative">
|
|
62
|
-
<div class="padding border round dashed surface-variant" style="min-height: 150px; display: flex; align-items: center; justify-content: center;">
|
|
63
|
-
<img id="image-preview" src="{% if recipe.image %}{{ recipe.image.url }}{% endif %}" class="responsive round" style="max-height: 200px; {% if not recipe.image %}display:none;{% endif %}">
|
|
74
|
+
<div class="padding border round dashed surface-variant" style="min-height: 150px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
|
75
|
+
<img id="image-preview" src="{% if recipe.image %}{{ recipe.image.url }}{% endif %}" class="responsive round mb-1" style="max-height: 200px; {% if not recipe.image %}display:none;{% endif %}">
|
|
64
76
|
<span id="image-placeholder" class="gray-text" {% if recipe.image %}style="display:none;"{% endif %}>{% trans "Image Preview" %}</span>
|
|
77
|
+
<button type="button" id="edit-image-btn" class="button transparent border round" style="display: none;" onclick="openCropper()">
|
|
78
|
+
<i>crop_rotate</i>
|
|
79
|
+
<span>{% trans "Edit Image" %}</span>
|
|
80
|
+
</button>
|
|
65
81
|
</div>
|
|
66
82
|
</div>
|
|
67
83
|
|
|
@@ -101,6 +117,41 @@
|
|
|
101
117
|
</div>
|
|
102
118
|
</article>
|
|
103
119
|
|
|
120
|
+
<dialog id="cropper-dialog" class="large">
|
|
121
|
+
<div class="padding">
|
|
122
|
+
<h5 class="bold mb-1">{% trans "Edit Image" %}</h5>
|
|
123
|
+
<div class="cropper-container mb-1">
|
|
124
|
+
<img id="cropper-image" src="">
|
|
125
|
+
</div>
|
|
126
|
+
<div class="row scroll no-space border round mb-1">
|
|
127
|
+
<button type="button" class="button transparent max" onclick="cropper.rotate(-90)" title="{% trans 'Rotate Left' %}">
|
|
128
|
+
<i>rotate_left</i>
|
|
129
|
+
</button>
|
|
130
|
+
<button type="button" class="button transparent max" onclick="cropper.rotate(90)" title="{% trans 'Rotate Right' %}">
|
|
131
|
+
<i>rotate_right</i>
|
|
132
|
+
</button>
|
|
133
|
+
<div class="divider vertical"></div>
|
|
134
|
+
<button type="button" class="button transparent max" onclick="cropper.scaleX(-cropper.getData().scaleX || -1)" title="{% trans 'Flip Horizontal' %}">
|
|
135
|
+
<i>flip</i>
|
|
136
|
+
</button>
|
|
137
|
+
<button type="button" class="button transparent max" onclick="cropper.scaleY(-cropper.getData().scaleY || -1)" title="{% trans 'Flip Vertical' %}">
|
|
138
|
+
<i>flip</i>
|
|
139
|
+
</button>
|
|
140
|
+
<div class="divider vertical"></div>
|
|
141
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(1)" title="{% trans '1:1' %}">1:1</button>
|
|
142
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(4/3)" title="{% trans '4:3' %}">4:3</button>
|
|
143
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(16/9)" title="{% trans '16:9' %}">16:9</button>
|
|
144
|
+
<button type="button" class="button transparent max" onclick="cropper.setAspectRatio(NaN)" title="{% trans 'Free' %}">
|
|
145
|
+
<i>crop_free</i>
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
<nav class="right-align">
|
|
149
|
+
<button type="button" class="button transparent" onclick="ui('#cropper-dialog')">{% trans "Cancel" %}</button>
|
|
150
|
+
<button type="button" class="button primary" onclick="applyCrop()">{% trans "Apply" %}</button>
|
|
151
|
+
</nav>
|
|
152
|
+
</div>
|
|
153
|
+
</dialog>
|
|
154
|
+
|
|
104
155
|
|
|
105
156
|
<div class="large-space"></div>
|
|
106
157
|
|
|
@@ -117,21 +168,67 @@
|
|
|
117
168
|
|
|
118
169
|
{% block page_scripts %}
|
|
119
170
|
<script>
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
let cropper;
|
|
172
|
+
const imageInput = document.querySelector('input[type="file"]');
|
|
173
|
+
const imagePreview = document.getElementById('image-preview');
|
|
174
|
+
const imagePlaceholder = document.getElementById('image-placeholder');
|
|
175
|
+
const editImageBtn = document.getElementById('edit-image-btn');
|
|
176
|
+
const imageDataInput = document.getElementsByName('image_data')[0];
|
|
177
|
+
const cropperImage = document.getElementById('cropper-image');
|
|
178
|
+
|
|
179
|
+
function openCropper() {
|
|
180
|
+
if (!imagePreview.src || imagePreview.src === window.location.href) return;
|
|
181
|
+
cropperImage.src = imagePreview.src;
|
|
182
|
+
ui('#cropper-dialog');
|
|
183
|
+
|
|
184
|
+
if (cropper) {
|
|
185
|
+
cropper.destroy();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
cropper = new Cropper(cropperImage, {
|
|
190
|
+
viewMode: 1,
|
|
191
|
+
autoCropArea: 1,
|
|
192
|
+
responsive: true,
|
|
193
|
+
restore: false,
|
|
194
|
+
checkCrossOrigin: true,
|
|
195
|
+
guides: true,
|
|
196
|
+
center: true,
|
|
197
|
+
highlight: false,
|
|
198
|
+
cropBoxMovable: true,
|
|
199
|
+
cropBoxResizable: true,
|
|
200
|
+
toggleDragModeOnDblclick: false,
|
|
201
|
+
});
|
|
202
|
+
}, 100);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applyCrop() {
|
|
206
|
+
const canvas = cropper.getCroppedCanvas({
|
|
207
|
+
maxWidth: 2000,
|
|
208
|
+
maxHeight: 2000,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const croppedData = canvas.toDataURL('image/jpeg', 0.9);
|
|
212
|
+
imagePreview.src = croppedData;
|
|
213
|
+
imagePreview.style.display = 'block';
|
|
214
|
+
if (imagePlaceholder) imagePlaceholder.style.display = 'none';
|
|
215
|
+
if (editImageBtn) editImageBtn.style.display = 'inline-flex';
|
|
216
|
+
imageDataInput.value = croppedData;
|
|
217
|
+
|
|
218
|
+
ui('#cropper-dialog');
|
|
219
|
+
}
|
|
220
|
+
|
|
122
221
|
if (imageInput) {
|
|
123
222
|
imageInput.onchange = function (evt) {
|
|
124
|
-
|
|
125
|
-
files = tgt.files;
|
|
126
|
-
|
|
223
|
+
const files = evt.target.files;
|
|
127
224
|
if (FileReader && files && files.length) {
|
|
128
|
-
|
|
225
|
+
const fr = new FileReader();
|
|
129
226
|
fr.onload = function () {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
227
|
+
imagePreview.src = fr.result;
|
|
228
|
+
imagePreview.style.display = 'block';
|
|
229
|
+
if (imagePlaceholder) imagePlaceholder.style.display = 'none';
|
|
230
|
+
if (editImageBtn) editImageBtn.style.display = 'inline-flex';
|
|
231
|
+
openCropper();
|
|
135
232
|
}
|
|
136
233
|
fr.readAsDataURL(files[0]);
|
|
137
234
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{% extends "base_beer.html" %}
|
|
2
|
+
{% load static i18n %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{% trans "Order Details" %} #{{ order.id }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="large-space"></div>
|
|
8
|
+
|
|
9
|
+
<div class="grid">
|
|
10
|
+
<div class="s12 m10 l8 xl6 middle-align center-align" style="margin: 0 auto;">
|
|
11
|
+
<article class="round elevate left-align">
|
|
12
|
+
<div class="padding">
|
|
13
|
+
<nav>
|
|
14
|
+
<a href="{% url 'user_profile' %}" class="button transparent circle">
|
|
15
|
+
<i>arrow_back</i>
|
|
16
|
+
</a>
|
|
17
|
+
<h5 class="max">{% trans "Order" %} #{{ order.id }}</h5>
|
|
18
|
+
<span class="chip {% if order.status == 'PENDING' %}surface-variant{% elif order.status == 'COMPLETED' %}primary{% elif order.status == 'CANCELLED' %}error{% else %}secondary{% endif %}">
|
|
19
|
+
{{ order.get_status_display }}
|
|
20
|
+
</span>
|
|
21
|
+
</nav>
|
|
22
|
+
<div class="divider"></div>
|
|
23
|
+
|
|
24
|
+
<div class="grid">
|
|
25
|
+
<div class="s12 m6">
|
|
26
|
+
<p class="small-text">{% trans "Date Ordered" %}</p>
|
|
27
|
+
<p>{{ order.created_at|date:"d F Y, H:i" }}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="s12 m6">
|
|
30
|
+
<p class="small-text">{% trans "Last Update" %}</p>
|
|
31
|
+
<p>{{ order.updated_at|date:"d F Y, H:i" }}</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="divider"></div>
|
|
36
|
+
|
|
37
|
+
<h6>{% trans "Item Details" %}</h6>
|
|
38
|
+
<div class="row">
|
|
39
|
+
<div class="max">
|
|
40
|
+
<a href="{% url 'recipe_detail' order.recipe.slug %}" class="bold primary-text">{{ order.recipe.title }}</a>
|
|
41
|
+
<p class="small-text">{{ order.recipe.description|truncatewords:20 }}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="min">
|
|
44
|
+
<span class="bold">{{ order.total_price }} €</span>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{% if order.recipe.image %}
|
|
49
|
+
<div class="space"></div>
|
|
50
|
+
<img src="{{ order.recipe.image.url }}" class="responsive round border" alt="{{ order.recipe.title }}">
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
<div class="divider"></div>
|
|
54
|
+
|
|
55
|
+
<div class="row">
|
|
56
|
+
<div class="max text-right">
|
|
57
|
+
<span class="bold">{% trans "Total" %}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="min">
|
|
60
|
+
<span class="bold text-primary">{{ order.total_price }} €</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
</div>
|
|
65
|
+
</article>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
{% endblock %}
|
|
@@ -90,6 +90,96 @@
|
|
|
90
90
|
|
|
91
91
|
</form>
|
|
92
92
|
</article>
|
|
93
|
+
|
|
94
|
+
<div class="large-space"></div>
|
|
95
|
+
|
|
96
|
+
<h4 class="center-align primary-text">{% trans "Order History" %}</h4>
|
|
97
|
+
|
|
98
|
+
<form method="get" class="row no-wrap middle-align">
|
|
99
|
+
<div class="field label border round small">
|
|
100
|
+
<select name="status" onchange="this.form.submit()">
|
|
101
|
+
<option value="">{% trans "All Statuses" %}</option>
|
|
102
|
+
{% for code, label in status_choices %}
|
|
103
|
+
<option value="{{ code }}" {% if current_status == code %}selected{% endif %}>{{ label }}</option>
|
|
104
|
+
{% endfor %}
|
|
105
|
+
</select>
|
|
106
|
+
<label>{% trans "Filter by Status" %}</label>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="space"></div>
|
|
109
|
+
<div class="field label border round small">
|
|
110
|
+
<select name="sort" onchange="this.form.submit()">
|
|
111
|
+
<option value="date_desc" {% if current_sort == 'date_desc' %}selected{% endif %}>{% trans "Newest First" %}</option>
|
|
112
|
+
<option value="date_asc" {% if current_sort == 'date_asc' %}selected{% endif %}>{% trans "Oldest First" %}</option>
|
|
113
|
+
<option value="price_desc" {% if current_sort == 'price_desc' %}selected{% endif %}>{% trans "Price: High to Low" %}</option>
|
|
114
|
+
<option value="price_asc" {% if current_sort == 'price_asc' %}selected{% endif %}>{% trans "Price: Low to High" %}</option>
|
|
115
|
+
</select>
|
|
116
|
+
<label>{% trans "Sort by" %}</label>
|
|
117
|
+
</div>
|
|
118
|
+
</form>
|
|
119
|
+
|
|
120
|
+
{% if orders %}
|
|
121
|
+
<div class="padding border round surface left-align">
|
|
122
|
+
<table class="border striped">
|
|
123
|
+
<thead>
|
|
124
|
+
<tr>
|
|
125
|
+
<th class="min">#</th>
|
|
126
|
+
<th class="max">{% trans "Recipe" %}</th>
|
|
127
|
+
<th class="min">{% trans "Date" %}</th>
|
|
128
|
+
<th class="min">{% trans "Status" %}</th>
|
|
129
|
+
<th class="min">{% trans "Price" %}</th>
|
|
130
|
+
<th class="min"></th>
|
|
131
|
+
</tr>
|
|
132
|
+
</thead>
|
|
133
|
+
<tbody>
|
|
134
|
+
{% for order in orders %}
|
|
135
|
+
<tr>
|
|
136
|
+
<td>{{ order.id }}</td>
|
|
137
|
+
<td>
|
|
138
|
+
<a href="{% url 'recipe_detail' order.recipe.slug %}">{{ order.recipe.title }}</a>
|
|
139
|
+
</td>
|
|
140
|
+
<td class="no-wrap">{{ order.created_at|date:"d/m/Y" }}</td>
|
|
141
|
+
<td>
|
|
142
|
+
<span class="chip tiny {% if order.status == 'PENDING' %}surface-variant{% elif order.status == 'COMPLETED' %}primary{% elif order.status == 'CANCELLED' %}error{% else %}secondary{% endif %}">
|
|
143
|
+
{{ order.get_status_display }}
|
|
144
|
+
</span>
|
|
145
|
+
</td>
|
|
146
|
+
<td class="no-wrap">{{ order.total_price }} €</td>
|
|
147
|
+
<td>
|
|
148
|
+
<a href="{% url 'user_order_detail' order.id %}" class="button circle transparent small">
|
|
149
|
+
<i>visibility</i>
|
|
150
|
+
</a>
|
|
151
|
+
</td>
|
|
152
|
+
</tr>
|
|
153
|
+
{% endfor %}
|
|
154
|
+
</tbody>
|
|
155
|
+
</table>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{% if orders.paginator.num_pages > 1 %}
|
|
159
|
+
<div class="center-align padding">
|
|
160
|
+
<nav class="row">
|
|
161
|
+
{% if orders.has_previous %}
|
|
162
|
+
<a href="?page={{ orders.previous_page_number }}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_sort %}&sort={{ current_sort }}{% endif %}" class="button transparent circle"><i>chevron_left</i></a>
|
|
163
|
+
{% else %}
|
|
164
|
+
<button class="button transparent circle" disabled><i>chevron_left</i></button>
|
|
165
|
+
{% endif %}
|
|
166
|
+
|
|
167
|
+
<span class="padding">{{ orders.number }} / {{ orders.paginator.num_pages }}</span>
|
|
168
|
+
|
|
169
|
+
{% if orders.has_next %}
|
|
170
|
+
<a href="?page={{ orders.next_page_number }}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_sort %}&sort={{ current_sort }}{% endif %}" class="button transparent circle"><i>chevron_right</i></a>
|
|
171
|
+
{% else %}
|
|
172
|
+
<button class="button transparent circle" disabled><i>chevron_right</i></button>
|
|
173
|
+
{% endif %}
|
|
174
|
+
</nav>
|
|
175
|
+
</div>
|
|
176
|
+
{% endif %}
|
|
177
|
+
|
|
178
|
+
{% else %}
|
|
179
|
+
<div class="padding border round surface">
|
|
180
|
+
<p class="center-align">{% trans "No previous orders found." %}</p>
|
|
181
|
+
</div>
|
|
182
|
+
{% endif %}
|
|
93
183
|
</div>
|
|
94
184
|
</div>
|
|
95
185
|
{% endblock %}
|
sandwitches/urls.py
CHANGED
|
@@ -34,6 +34,7 @@ urlpatterns = [
|
|
|
34
34
|
path("login/", views.CustomLoginView.as_view(), name="login"),
|
|
35
35
|
path("logout/", LogoutView.as_view(next_page="index"), name="logout"),
|
|
36
36
|
path("profile/", views.user_profile, name="user_profile"),
|
|
37
|
+
path("orders/<int:pk>/", views.user_order_detail, name="user_order_detail"),
|
|
37
38
|
path("community/", views.community, name="community"),
|
|
38
39
|
path("admin/", admin.site.urls),
|
|
39
40
|
path("api/", api.urls),
|
sandwitches/views.py
CHANGED
|
@@ -28,6 +28,7 @@ from PIL import Image
|
|
|
28
28
|
from django.db.models import Q, Avg
|
|
29
29
|
from django_tasks.backends.database.models import DBTaskResult
|
|
30
30
|
from django.contrib.auth.views import LoginView
|
|
31
|
+
from django.core.paginator import Paginator
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
from sandwitches import __version__ as sandwitches_version
|
|
@@ -858,8 +859,52 @@ def user_profile(request):
|
|
|
858
859
|
return redirect("user_profile")
|
|
859
860
|
else:
|
|
860
861
|
form = UserProfileForm(instance=request.user)
|
|
862
|
+
|
|
863
|
+
orders = request.user.orders.select_related("recipe").all()
|
|
864
|
+
|
|
865
|
+
# Filtering
|
|
866
|
+
status_filter = request.GET.get("status")
|
|
867
|
+
if status_filter:
|
|
868
|
+
orders = orders.filter(status=status_filter)
|
|
869
|
+
|
|
870
|
+
# Sorting
|
|
871
|
+
sort_param = request.GET.get("sort", "-created_at")
|
|
872
|
+
allowed_sorts = {
|
|
873
|
+
"date_asc": "created_at",
|
|
874
|
+
"date_desc": "-created_at",
|
|
875
|
+
"price_asc": "total_price",
|
|
876
|
+
"price_desc": "-total_price",
|
|
877
|
+
"status": "status",
|
|
878
|
+
}
|
|
879
|
+
order_by = allowed_sorts.get(sort_param, "-created_at")
|
|
880
|
+
orders = orders.order_by(order_by)
|
|
881
|
+
|
|
882
|
+
# Pagination
|
|
883
|
+
paginator = Paginator(orders, 5) # Show 5 orders per page
|
|
884
|
+
page_number = request.GET.get("page")
|
|
885
|
+
page_obj = paginator.get_page(page_number)
|
|
886
|
+
|
|
861
887
|
return render(
|
|
862
|
-
request,
|
|
888
|
+
request,
|
|
889
|
+
"profile.html",
|
|
890
|
+
{
|
|
891
|
+
"form": form,
|
|
892
|
+
"version": sandwitches_version,
|
|
893
|
+
"orders": page_obj,
|
|
894
|
+
"current_status": status_filter,
|
|
895
|
+
"current_sort": sort_param,
|
|
896
|
+
"status_choices": Order.STATUS_CHOICES,
|
|
897
|
+
},
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
@login_required
|
|
902
|
+
def user_order_detail(request, pk):
|
|
903
|
+
order = get_object_or_404(Order, pk=pk, user=request.user)
|
|
904
|
+
return render(
|
|
905
|
+
request,
|
|
906
|
+
"order_detail.html",
|
|
907
|
+
{"order": order, "version": sandwitches_version},
|
|
863
908
|
)
|
|
864
909
|
|
|
865
910
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
sandwitches/__init__.py,sha256=YTDsQDSdJmxV2Z0dTbBqZhuJRuXcNLSKL0SX73Lu2u8,195
|
|
2
|
-
sandwitches/admin.py,sha256=-
|
|
2
|
+
sandwitches/admin.py,sha256=-4QA5InEvLHyb6VFAGKapWbJO9mXdiV4GeQcGsM4xlI,2510
|
|
3
3
|
sandwitches/api.py,sha256=ruD5QeOPY-l9PvkJQiaOYoI0sRARDpqpFrFDgBxo9cQ,6389
|
|
4
4
|
sandwitches/asgi.py,sha256=cygnXdXSSVspM7ZXuj47Ef6oz7HSTw4D7BPzgE2PU5w,399
|
|
5
5
|
sandwitches/feeds.py,sha256=iz1d11dV0utA0ZNsB7VIAp0h8Zr5mFNSKJWHbw_j6YM,683
|
|
6
|
-
sandwitches/forms.py,sha256=
|
|
6
|
+
sandwitches/forms.py,sha256=VoQ81COBCgwuS161dhF8IVeeAp8dLiQakzs56SV6-T8,9541
|
|
7
7
|
sandwitches/locale/nl/LC_MESSAGES/django.mo,sha256=EzQWzIhz_Na3w9AS7F-YjB-Xv63t4sMRSAkEQ1-g32M,5965
|
|
8
8
|
sandwitches/locale/nl/LC_MESSAGES/django.po,sha256=znxspEoMwkmktusZtbVrt1KG1LDUwIEi4ZEIE3XGeoI,25904
|
|
9
9
|
sandwitches/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -23,8 +23,9 @@ sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py
|
|
|
23
23
|
sandwitches/migrations/0012_rename_is_community_made_historicalrecipe_is_approved_and_more.py,sha256=bCDPpHmZTIW70-YeL30WhuJ2mORktkrsntKqTw0vj94,577
|
|
24
24
|
sandwitches/migrations/0013_cartitem.py,sha256=KYMinpnZiLHwjo7p7EdJHQExuEGC9jtpcZcbm1r7JFo,1787
|
|
25
25
|
sandwitches/migrations/0014_ensure_groups_exist.py,sha256=5FSA742bEQtwHZl5CWZQYIdmS8FBxMgWS079dOaOltY,564
|
|
26
|
+
sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py,sha256=PTXQZUE8RqTAK8l0vkZhiGKv2T0PDiWEue7f6qz3AQ0,1670
|
|
26
27
|
sandwitches/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
sandwitches/models.py,sha256=
|
|
28
|
+
sandwitches/models.py,sha256=kU71fC9Lf6Ij_8vpN0nB0OUKWpzWxi9WxqIYEzP4Ogg,11144
|
|
28
29
|
sandwitches/settings.py,sha256=5_eQAJCAV093hnhr3XOxHekT4IF-PEJcRiTecq71_SQ,5841
|
|
29
30
|
sandwitches/storage.py,sha256=ibBG6tVtArqzgEKsRimZPwsqW7i9j4WiPLLHrOJchow,3578
|
|
30
31
|
sandwitches/tasks.py,sha256=YiliAT2rj0fh7hrwKq5_qWtv9AGhd5iulj_iBwZBBKg,6024
|
|
@@ -33,10 +34,10 @@ sandwitches/templates/admin/confirm_delete.html,sha256=HfsZI_gV8JQTKz215TYgPWBrg
|
|
|
33
34
|
sandwitches/templates/admin/dashboard.html,sha256=Ial8zH2odIPpstSkQmzGrasl0QxvgGhFPAGy7V5xRzY,5916
|
|
34
35
|
sandwitches/templates/admin/order_list.html,sha256=eHFUn2speXaaj5_SFUG0Z0HfWVUR9-VCDRBeb8ufFb0,819
|
|
35
36
|
sandwitches/templates/admin/partials/dashboard_charts.html,sha256=NYrt-LDZO4__2KDWhAYL5K_f-2Zgj0iiuaZQiRZlBWg,3639
|
|
36
|
-
sandwitches/templates/admin/partials/order_rows.html,sha256=
|
|
37
|
+
sandwitches/templates/admin/partials/order_rows.html,sha256=C7_ArHw1udaGjx6CRJHhksje0OReP7UUhdsHcdFPqCc,940
|
|
37
38
|
sandwitches/templates/admin/rating_list.html,sha256=8CHAsBfKfs4izhb-IyOiDjJXqAZxFcStoRSGh4pRlgM,1365
|
|
38
39
|
sandwitches/templates/admin/recipe_approval_list.html,sha256=M6GFYI45lAkLkvqP44cu5tDYVOeeVNklEphof1euesM,2281
|
|
39
|
-
sandwitches/templates/admin/recipe_form.html,sha256=
|
|
40
|
+
sandwitches/templates/admin/recipe_form.html,sha256=wVKKBFl3vN11aknnmv2Hxkj66zZk9iZ0x_iS1j_X_Ro,12884
|
|
40
41
|
sandwitches/templates/admin/recipe_list.html,sha256=5fGnRIQ7JfvM3yfG-sngEIEgiPnPDkjK1Tn3nO8EDh4,5359
|
|
41
42
|
sandwitches/templates/admin/tag_form.html,sha256=JRWgAl4fz_Oy-Kuo1K6Mex_CXdsHMABzzyPazthr1Kg,989
|
|
42
43
|
sandwitches/templates/admin/tag_list.html,sha256=ttxwXgfdxkEs4Cmrz5RHaGmaqLd7JDmWhjv80XIQqyw,1246
|
|
@@ -47,7 +48,7 @@ sandwitches/templates/admin/user_list.html,sha256=6O1YctULY-tqJnagybJof9ERA_NL1L
|
|
|
47
48
|
sandwitches/templates/base.html,sha256=mwCESNirfvvdyMg2e1Siy_LA8fLH29m0aS_Jv0Qom4U,3597
|
|
48
49
|
sandwitches/templates/base_beer.html,sha256=4QgU4_gu_RRMtimmRAhATDJ3mj_WANxtilQJYNgAL60,2077
|
|
49
50
|
sandwitches/templates/cart.html,sha256=YqmrzOLLPAXSqeXeUTrt9AwTTWOitOLTaD_k3mYYVpM,4537
|
|
50
|
-
sandwitches/templates/community.html,sha256
|
|
51
|
+
sandwitches/templates/community.html,sha256=-YhpPtLbrVK9mc2Go1XBInLK-7OXrtb7kKukjl7rGbg,9607
|
|
51
52
|
sandwitches/templates/components/carousel_scripts.html,sha256=9vEL5JJv8zUUjEtsnHW-BwwXUNWqQ6w_vf6UdxgEv_I,1934
|
|
52
53
|
sandwitches/templates/components/favorites_search_form.html,sha256=tpD8SpS47TUDJBwxhMuvjhTN9pjWoRGFW50TBv48Ld4,5202
|
|
53
54
|
sandwitches/templates/components/footer.html,sha256=Qk-myRtXS6-1b3fMowVGnSuFb_UkUgX6BYX9zgh_SV8,486
|
|
@@ -66,17 +67,18 @@ sandwitches/templates/detail.html,sha256=g-O_RsW9Ix9ivWC0nZ4FwHY2NhgYZ3bEGLpqGY0
|
|
|
66
67
|
sandwitches/templates/favorites.html,sha256=0cPpW07N6Isrb8XpvA5Eh97L2-12QFZ43EzeJvbOlXo,917
|
|
67
68
|
sandwitches/templates/index.html,sha256=7anU7k8s80JYk59Rwsm8EdlNYd7B5clCvV7pKq2IUy0,2518
|
|
68
69
|
sandwitches/templates/login.html,sha256=LiQskhkOkfx0EE4ssA1ToqQ3oEll08OPYLDIkLjHfU8,2177
|
|
70
|
+
sandwitches/templates/order_detail.html,sha256=D6MjUVibQuED2VRNHSjKVnLHcLgFtLvcVmuwlzfoJzo,2498
|
|
69
71
|
sandwitches/templates/partials/recipe_list.html,sha256=LUHKFKG90D72K9X2X3d1osvj2jX1QU_MbPe0lNwRSII,4555
|
|
70
|
-
sandwitches/templates/profile.html,sha256=
|
|
72
|
+
sandwitches/templates/profile.html,sha256=m3-31b_z5QhHLgol-QwHrb3MM9B9kk5kybL3B1nWC5Y,8539
|
|
71
73
|
sandwitches/templates/setup.html,sha256=iNveFgePATsCSO4XMbGPa8TnWHyvj8S_5WwcW6i7pbo,4661
|
|
72
74
|
sandwitches/templates/signup.html,sha256=pNBSlRGZI_B5ccF3dWpUgWBcjODkdLlq7HhyJLYIHCI,6176
|
|
73
75
|
sandwitches/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
76
|
sandwitches/templatetags/custom_filters.py,sha256=0KDFlFz4b5LwlcURBAmzyYWKKea-LwydZytJGVkkuKA,243
|
|
75
77
|
sandwitches/templatetags/markdown_extras.py,sha256=0ibmRzxE3r85x4k7kK71R-9UT0CgeegYF7MHzj3juTI,344
|
|
76
|
-
sandwitches/urls.py,sha256=
|
|
78
|
+
sandwitches/urls.py,sha256=TysY6JhOV2kGC-9KloZjLDfk4TUZS6xX95A_RfG971g,4940
|
|
77
79
|
sandwitches/utils.py,sha256=SJP-TkeRZ0OIfaMigYrOSbxRqYXswoqoWhwll3nFuAM,7245
|
|
78
|
-
sandwitches/views.py,sha256=
|
|
80
|
+
sandwitches/views.py,sha256=pDDDXaojPstj5E_k5gBQmW901Lol0R3gUl3qWGwUmI0,32145
|
|
79
81
|
sandwitches/wsgi.py,sha256=Eyncpnahq_4s3Lr9ruB-R3Lu9j9zBXqgPbUj7qhIbwU,399
|
|
80
|
-
sandwitches-2.4.
|
|
81
|
-
sandwitches-2.4.
|
|
82
|
-
sandwitches-2.4.
|
|
82
|
+
sandwitches-2.4.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
83
|
+
sandwitches-2.4.1.dist-info/METADATA,sha256=97JvkhJ7rzJpQIfULxJ-0yz3rN1EfGZkhB-zpihx4wg,3111
|
|
84
|
+
sandwitches-2.4.1.dist-info/RECORD,,
|
|
File without changes
|