sandwitches 2.4.0__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sandwitches/admin.py +10 -2
- sandwitches/forms.py +78 -13
- sandwitches/migrations/0015_order_completed_alter_order_status_and_more.py +56 -0
- sandwitches/migrations/0016_user_theme.py +21 -0
- sandwitches/migrations/0017_setting_gotify_token_setting_gotify_url.py +31 -0
- sandwitches/models.py +47 -2
- sandwitches/tasks.py +31 -1
- sandwitches/templates/admin/admin_base.html +0 -3
- sandwitches/templates/admin/partials/order_rows.html +1 -1
- sandwitches/templates/admin/recipe_form.html +113 -18
- sandwitches/templates/base.html +1 -1
- sandwitches/templates/base_beer.html +0 -1
- sandwitches/templates/community.html +111 -14
- sandwitches/templates/components/navbar.html +0 -6
- sandwitches/templates/components/side_menu.html +4 -0
- sandwitches/templates/components/user_menu.html +1 -0
- sandwitches/templates/order_detail.html +68 -0
- sandwitches/templates/profile.html +91 -0
- sandwitches/templates/settings.html +53 -0
- sandwitches/templates/signup.html +0 -12
- sandwitches/urls.py +2 -0
- sandwitches/views.py +74 -1
- {sandwitches-2.4.0.dist-info → sandwitches-2.5.0.dist-info}/METADATA +1 -1
- {sandwitches-2.4.0.dist-info → sandwitches-2.5.0.dist-info}/RECORD +25 -21
- sandwitches/templates/components/language_dialog.html +0 -26
- {sandwitches-2.4.0.dist-info → sandwitches-2.5.0.dist-info}/WHEEL +0 -0
sandwitches/templates/base.html
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
</style>
|
|
48
48
|
{% block extra_head %}{% endblock %}
|
|
49
49
|
</head>
|
|
50
|
-
<body class="{% block body_class %}{% endblock %}">
|
|
50
|
+
<body class="{% block body_class %}{% endblock %} {% if user.is_authenticated %}{{ user.theme }}{% endif %}">
|
|
51
51
|
<div id="loading-sandwich" class="loading-sandwich-container">
|
|
52
52
|
<svg class="loading-sandwich-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
53
53
|
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
|
|
@@ -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
|
}
|
|
@@ -7,12 +7,6 @@
|
|
|
7
7
|
<img src="{% static 'icons/icon.svg' %}" class="circle small">
|
|
8
8
|
</a>
|
|
9
9
|
<div class="max"></div>
|
|
10
|
-
<button class="circle transparent" onclick="ui('mode', ui('mode') == 'dark' ? 'light' : 'dark')">
|
|
11
|
-
<i>dark_mode</i>
|
|
12
|
-
</button>
|
|
13
|
-
<button class="circle transparent" data-ui="#language-menu">
|
|
14
|
-
<i>language</i>
|
|
15
|
-
</button>
|
|
16
10
|
|
|
17
11
|
{% if user.is_authenticated %}
|
|
18
12
|
<a href="{% url 'view_cart' %}" class="button circle transparent">
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
<i class="extra padding">group</i>
|
|
20
20
|
<span class="large-text">{% trans "Community" %}</span>
|
|
21
21
|
</a>
|
|
22
|
+
<a href="{% url 'user_settings' %}" class="padding {% if request.resolver_match.url_name == 'user_settings' %}active{% endif %}">
|
|
23
|
+
<i class="extra padding">settings</i>
|
|
24
|
+
<span class="large-text">{% trans "Settings" %}</span>
|
|
25
|
+
</a>
|
|
22
26
|
{% endif %}
|
|
23
27
|
<a href="/api/docs" class="padding">
|
|
24
28
|
<i class="extra padding">api</i>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{% load i18n %}
|
|
2
2
|
{% if user.is_authenticated %}
|
|
3
3
|
<menu id="user-menu" class="no-wrap left">
|
|
4
|
+
<a href="{% url 'user_settings' %}" class="row"><i>settings</i>{% trans "Settings" %}</a>
|
|
4
5
|
{% if user.is_staff %}
|
|
5
6
|
<a href="{% url 'admin_dashboard' %}" class="row"><i>admin_panel_settings</i>{% trans "Admin" %}</a>
|
|
6
7
|
<div class="divider"></div>
|
|
@@ -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 %}
|
|
@@ -84,12 +84,103 @@
|
|
|
84
84
|
<div class="large-space"></div>
|
|
85
85
|
|
|
86
86
|
<nav class="right-align">
|
|
87
|
+
<a class="button transparent border round" href="{% url 'user_settings' %}"><i>settings</i> {% trans "Settings" %}</a>
|
|
87
88
|
<a class="button transparent border round" href="{% url 'index' %}">{% trans "Cancel" %}</a>
|
|
88
89
|
<button type="submit" class="button primary round">{% trans "Save changes" %}</button>
|
|
89
90
|
</nav>
|
|
90
91
|
|
|
91
92
|
</form>
|
|
92
93
|
</article>
|
|
94
|
+
|
|
95
|
+
<div class="large-space"></div>
|
|
96
|
+
|
|
97
|
+
<h4 class="center-align primary-text">{% trans "Order History" %}</h4>
|
|
98
|
+
|
|
99
|
+
<form method="get" class="row no-wrap middle-align">
|
|
100
|
+
<div class="field label border round small">
|
|
101
|
+
<select name="status" onchange="this.form.submit()">
|
|
102
|
+
<option value="">{% trans "All Statuses" %}</option>
|
|
103
|
+
{% for code, label in status_choices %}
|
|
104
|
+
<option value="{{ code }}" {% if current_status == code %}selected{% endif %}>{{ label }}</option>
|
|
105
|
+
{% endfor %}
|
|
106
|
+
</select>
|
|
107
|
+
<label>{% trans "Filter by Status" %}</label>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="space"></div>
|
|
110
|
+
<div class="field label border round small">
|
|
111
|
+
<select name="sort" onchange="this.form.submit()">
|
|
112
|
+
<option value="date_desc" {% if current_sort == 'date_desc' %}selected{% endif %}>{% trans "Newest First" %}</option>
|
|
113
|
+
<option value="date_asc" {% if current_sort == 'date_asc' %}selected{% endif %}>{% trans "Oldest First" %}</option>
|
|
114
|
+
<option value="price_desc" {% if current_sort == 'price_desc' %}selected{% endif %}>{% trans "Price: High to Low" %}</option>
|
|
115
|
+
<option value="price_asc" {% if current_sort == 'price_asc' %}selected{% endif %}>{% trans "Price: Low to High" %}</option>
|
|
116
|
+
</select>
|
|
117
|
+
<label>{% trans "Sort by" %}</label>
|
|
118
|
+
</div>
|
|
119
|
+
</form>
|
|
120
|
+
|
|
121
|
+
{% if orders %}
|
|
122
|
+
<div class="padding border round surface left-align">
|
|
123
|
+
<table class="border striped">
|
|
124
|
+
<thead>
|
|
125
|
+
<tr>
|
|
126
|
+
<th class="min">#</th>
|
|
127
|
+
<th class="max">{% trans "Recipe" %}</th>
|
|
128
|
+
<th class="min">{% trans "Date" %}</th>
|
|
129
|
+
<th class="min">{% trans "Status" %}</th>
|
|
130
|
+
<th class="min">{% trans "Price" %}</th>
|
|
131
|
+
<th class="min"></th>
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
{% for order in orders %}
|
|
136
|
+
<tr>
|
|
137
|
+
<td>{{ order.id }}</td>
|
|
138
|
+
<td>
|
|
139
|
+
<a href="{% url 'recipe_detail' order.recipe.slug %}">{{ order.recipe.title }}</a>
|
|
140
|
+
</td>
|
|
141
|
+
<td class="no-wrap">{{ order.created_at|date:"d/m/Y" }}</td>
|
|
142
|
+
<td>
|
|
143
|
+
<span class="chip tiny {% if order.status == 'PENDING' %}surface-variant{% elif order.status == 'COMPLETED' %}primary{% elif order.status == 'CANCELLED' %}error{% else %}secondary{% endif %}">
|
|
144
|
+
{{ order.get_status_display }}
|
|
145
|
+
</span>
|
|
146
|
+
</td>
|
|
147
|
+
<td class="no-wrap">{{ order.total_price }} €</td>
|
|
148
|
+
<td>
|
|
149
|
+
<a href="{% url 'user_order_detail' order.id %}" class="button circle transparent small">
|
|
150
|
+
<i>visibility</i>
|
|
151
|
+
</a>
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
{% endfor %}
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{% if orders.paginator.num_pages > 1 %}
|
|
160
|
+
<div class="center-align padding">
|
|
161
|
+
<nav class="row">
|
|
162
|
+
{% if orders.has_previous %}
|
|
163
|
+
<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>
|
|
164
|
+
{% else %}
|
|
165
|
+
<button class="button transparent circle" disabled><i>chevron_left</i></button>
|
|
166
|
+
{% endif %}
|
|
167
|
+
|
|
168
|
+
<span class="padding">{{ orders.number }} / {{ orders.paginator.num_pages }}</span>
|
|
169
|
+
|
|
170
|
+
{% if orders.has_next %}
|
|
171
|
+
<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>
|
|
172
|
+
{% else %}
|
|
173
|
+
<button class="button transparent circle" disabled><i>chevron_right</i></button>
|
|
174
|
+
{% endif %}
|
|
175
|
+
</nav>
|
|
176
|
+
</div>
|
|
177
|
+
{% endif %}
|
|
178
|
+
|
|
179
|
+
{% else %}
|
|
180
|
+
<div class="padding border round surface">
|
|
181
|
+
<p class="center-align">{% trans "No previous orders found." %}</p>
|
|
182
|
+
</div>
|
|
183
|
+
{% endif %}
|
|
93
184
|
</div>
|
|
94
185
|
</div>
|
|
95
186
|
{% endblock %}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{% extends "base_beer.html" %}
|
|
2
|
+
{% load static i18n %}
|
|
3
|
+
{% block title %}{% trans "Settings" %}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="large-space"></div>
|
|
7
|
+
|
|
8
|
+
<div class="grid">
|
|
9
|
+
<div class="s12 m8 l6 xl4 middle-align center-align" style="margin: 0 auto;">
|
|
10
|
+
<article class="round elevate">
|
|
11
|
+
<div class="padding">
|
|
12
|
+
<h4 class="center-align primary-text">{% trans "User Settings" %}</h4>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<form method="post" novalidate>
|
|
16
|
+
{% csrf_token %}
|
|
17
|
+
|
|
18
|
+
<div class="field label border round {% if form.language.errors %}error{% endif %}">
|
|
19
|
+
<select name="{{ form.language.name }}" id="{{ form.language.id_for_label }}">
|
|
20
|
+
{% for value, label in form.language.field.choices %}
|
|
21
|
+
<option value="{{ value }}" {% if form.language.value == value %}selected{% endif %}>{{ label }}</option>
|
|
22
|
+
{% endfor %}
|
|
23
|
+
</select>
|
|
24
|
+
<label>{% trans "Preferred Language" %}</label>
|
|
25
|
+
{% if form.language.errors %}
|
|
26
|
+
<span class="helper error-text">{{ form.language.errors.0 }}</span>
|
|
27
|
+
{% endif %}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="field label border round {% if form.theme.errors %}error{% endif %}">
|
|
31
|
+
<select name="{{ form.theme.name }}" id="{{ form.theme.id_for_label }}">
|
|
32
|
+
{% for value, label in form.theme.field.choices %}
|
|
33
|
+
<option value="{{ value }}" {% if form.theme.value == value %}selected{% endif %}>{{ label }}</option>
|
|
34
|
+
{% endfor %}
|
|
35
|
+
</select>
|
|
36
|
+
<label>{% trans "Preferred Theme" %}</label>
|
|
37
|
+
{% if form.theme.errors %}
|
|
38
|
+
<span class="helper error-text">{{ form.theme.errors.0 }}</span>
|
|
39
|
+
{% endif %}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="large-space"></div>
|
|
43
|
+
|
|
44
|
+
<nav class="right-align">
|
|
45
|
+
<a class="button transparent border round" href="{% url 'index' %}">{% trans "Cancel" %}</a>
|
|
46
|
+
<button type="submit" class="button primary round">{% trans "Save changes" %}</button>
|
|
47
|
+
</nav>
|
|
48
|
+
|
|
49
|
+
</form>
|
|
50
|
+
</article>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
{% endblock %}
|
|
@@ -64,18 +64,6 @@
|
|
|
64
64
|
{% endif %}
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
|
-
<div class="field label border round {% if form.language.errors %}error{% endif %}">
|
|
68
|
-
<select name="{{ form.language.name }}" id="{{ form.language.id_for_label }}">
|
|
69
|
-
{% for value, label in form.language.field.choices %}
|
|
70
|
-
<option value="{{ value }}" {% if form.language.value == value %}selected{% endif %}>{{ label }}</option>
|
|
71
|
-
{% endfor %}
|
|
72
|
-
</select>
|
|
73
|
-
<label>{% trans "Preferred Language" %}</label>
|
|
74
|
-
{% if form.language.errors %}
|
|
75
|
-
<span class="helper error-text">{{ form.language.errors.0 }}</span>
|
|
76
|
-
{% endif %}
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
67
|
<div class="field label border round textarea {% if form.bio.errors %}error{% endif %}">
|
|
80
68
|
<textarea name="{{ form.bio.name }}" id="{{ form.bio.id_for_label }}" rows="3">{{ form.bio.value|default:'' }}</textarea>
|
|
81
69
|
<label>{% trans "Bio" %}</label>
|
sandwitches/urls.py
CHANGED
|
@@ -34,6 +34,8 @@ 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("settings/", views.user_settings, name="user_settings"),
|
|
38
|
+
path("orders/<int:pk>/", views.user_order_detail, name="user_order_detail"),
|
|
37
39
|
path("community/", views.community, name="community"),
|
|
38
40
|
path("admin/", admin.site.urls),
|
|
39
41
|
path("api/", api.urls),
|
sandwitches/views.py
CHANGED
|
@@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model
|
|
|
8
8
|
from django.contrib.auth.decorators import login_required
|
|
9
9
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
10
10
|
from django.utils.translation import gettext as _
|
|
11
|
+
from django.utils import translation
|
|
11
12
|
from .models import Recipe, Rating, Tag, Order, CartItem
|
|
12
13
|
from .forms import (
|
|
13
14
|
RecipeForm,
|
|
@@ -18,6 +19,7 @@ from .forms import (
|
|
|
18
19
|
TagForm,
|
|
19
20
|
UserProfileForm,
|
|
20
21
|
UserRecipeSubmissionForm,
|
|
22
|
+
UserSettingsForm,
|
|
21
23
|
)
|
|
22
24
|
from django.http import HttpResponseBadRequest, Http404
|
|
23
25
|
from django.conf import settings
|
|
@@ -28,6 +30,7 @@ from PIL import Image
|
|
|
28
30
|
from django.db.models import Q, Avg
|
|
29
31
|
from django_tasks.backends.database.models import DBTaskResult
|
|
30
32
|
from django.contrib.auth.views import LoginView
|
|
33
|
+
from django.core.paginator import Paginator
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
from sandwitches import __version__ as sandwitches_version
|
|
@@ -858,8 +861,78 @@ def user_profile(request):
|
|
|
858
861
|
return redirect("user_profile")
|
|
859
862
|
else:
|
|
860
863
|
form = UserProfileForm(instance=request.user)
|
|
864
|
+
|
|
865
|
+
orders = request.user.orders.select_related("recipe").all()
|
|
866
|
+
|
|
867
|
+
# Filtering
|
|
868
|
+
status_filter = request.GET.get("status")
|
|
869
|
+
if status_filter:
|
|
870
|
+
orders = orders.filter(status=status_filter)
|
|
871
|
+
|
|
872
|
+
# Sorting
|
|
873
|
+
sort_param = request.GET.get("sort", "-created_at")
|
|
874
|
+
allowed_sorts = {
|
|
875
|
+
"date_asc": "created_at",
|
|
876
|
+
"date_desc": "-created_at",
|
|
877
|
+
"price_asc": "total_price",
|
|
878
|
+
"price_desc": "-total_price",
|
|
879
|
+
"status": "status",
|
|
880
|
+
}
|
|
881
|
+
order_by = allowed_sorts.get(sort_param, "-created_at")
|
|
882
|
+
orders = orders.order_by(order_by)
|
|
883
|
+
|
|
884
|
+
# Pagination
|
|
885
|
+
paginator = Paginator(orders, 5) # Show 5 orders per page
|
|
886
|
+
page_number = request.GET.get("page")
|
|
887
|
+
page_obj = paginator.get_page(page_number)
|
|
888
|
+
|
|
889
|
+
return render(
|
|
890
|
+
request,
|
|
891
|
+
"profile.html",
|
|
892
|
+
{
|
|
893
|
+
"form": form,
|
|
894
|
+
"version": sandwitches_version,
|
|
895
|
+
"orders": page_obj,
|
|
896
|
+
"current_status": status_filter,
|
|
897
|
+
"current_sort": sort_param,
|
|
898
|
+
"status_choices": Order.STATUS_CHOICES,
|
|
899
|
+
},
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@login_required
|
|
904
|
+
def user_settings(request):
|
|
905
|
+
if request.method == "POST":
|
|
906
|
+
form = UserSettingsForm(request.POST, instance=request.user)
|
|
907
|
+
if form.is_valid():
|
|
908
|
+
user = form.save()
|
|
909
|
+
# Update language in session and cookie
|
|
910
|
+
translation.activate(user.language)
|
|
911
|
+
request.session[translation.LANGUAGE_SESSION_KEY] = user.language # ty:ignore[unresolved-attribute]
|
|
912
|
+
messages.success(request, _("Settings updated successfully."))
|
|
913
|
+
response = redirect("user_settings")
|
|
914
|
+
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user.language)
|
|
915
|
+
return response
|
|
916
|
+
else:
|
|
917
|
+
form = UserSettingsForm(instance=request.user)
|
|
918
|
+
|
|
861
919
|
return render(
|
|
862
|
-
request,
|
|
920
|
+
request,
|
|
921
|
+
"settings.html",
|
|
922
|
+
{
|
|
923
|
+
"form": form,
|
|
924
|
+
"version": sandwitches_version,
|
|
925
|
+
},
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
@login_required
|
|
930
|
+
def user_order_detail(request, pk):
|
|
931
|
+
order = get_object_or_404(Order, pk=pk, user=request.user)
|
|
932
|
+
return render(
|
|
933
|
+
request,
|
|
934
|
+
"order_detail.html",
|
|
935
|
+
{"order": order, "version": sandwitches_version},
|
|
863
936
|
)
|
|
864
937
|
|
|
865
938
|
|