django-unfold 0.37.0__py3-none-any.whl → 0.39.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- django_unfold-0.39.0.dist-info/METADATA +70 -0
- {django_unfold-0.37.0.dist-info → django_unfold-0.39.0.dist-info}/RECORD +59 -57
- unfold/admin.py +4 -1
- unfold/contrib/filters/templates/unfold/filters/filters_date_range.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_range.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_single.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +4 -4
- unfold/contrib/forms/templates/unfold/forms/helpers/toolbar.html +6 -6
- unfold/contrib/guardian/templates/unfold/guardian/group_form.html +5 -5
- unfold/contrib/guardian/templates/unfold/guardian/user_form.html +5 -5
- unfold/contrib/import_export/templates/admin/import_export/change_form.html +1 -1
- unfold/contrib/import_export/templates/admin/import_export/import_errors.html +1 -1
- unfold/contrib/simple_history/templates/simple_history/object_history.html +1 -1
- unfold/contrib/simple_history/templates/simple_history/object_history_list.html +8 -8
- unfold/contrib/simple_history/templates/simple_history/submit_line.html +1 -1
- unfold/forms.py +5 -1
- unfold/settings.py +26 -1
- unfold/static/admin/js/admin/RelatedObjectLookups.js +295 -0
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/app.js +3 -1
- unfold/styles.css +1 -1
- unfold/templates/admin/app_list.html +2 -2
- unfold/templates/admin/change_form.html +8 -1
- unfold/templates/admin/change_list.html +1 -6
- unfold/templates/admin/change_list_results.html +2 -2
- unfold/templates/admin/delete_confirmation.html +5 -5
- unfold/templates/admin/delete_selected_confirmation.html +5 -5
- unfold/templates/admin/edit_inline/stacked.html +2 -2
- unfold/templates/admin/edit_inline/tabular.html +3 -3
- unfold/templates/admin/filter.html +1 -1
- unfold/templates/admin/includes/fieldset.html +1 -1
- unfold/templates/admin/includes/object_delete_summary.html +1 -1
- unfold/templates/admin/login.html +2 -2
- unfold/templates/admin/search_form.html +1 -1
- unfold/templates/auth/widgets/read_only_password_hash.html +2 -2
- unfold/templates/registration/logged_out.html +1 -1
- unfold/templates/unfold/change_list_filter.html +19 -3
- unfold/templates/unfold/components/button.html +1 -1
- unfold/templates/unfold/components/card.html +1 -1
- unfold/templates/unfold/components/navigation.html +1 -1
- unfold/templates/unfold/components/title.html +1 -1
- unfold/templates/unfold/helpers/actions_row.html +1 -1
- unfold/templates/unfold/helpers/app_list.html +7 -7
- unfold/templates/unfold/helpers/fieldsets_tabs.html +2 -2
- unfold/templates/unfold/helpers/form_label.html +1 -1
- unfold/templates/unfold/helpers/messages.html +1 -1
- unfold/templates/unfold/helpers/navigation.html +1 -1
- unfold/templates/unfold/helpers/search.html +1 -1
- unfold/templates/unfold/helpers/tab_action.html +1 -1
- unfold/templates/unfold/helpers/welcomemsg.html +3 -3
- unfold/templates/unfold/layouts/skeleton.html +1 -1
- unfold/templates/unfold/widgets/clearable_file_input.html +2 -2
- unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
- unfold/templates/unfold/widgets/url.html +7 -0
- unfold/templatetags/unfold_list.py +0 -7
- unfold/widgets.py +26 -6
- django_unfold-0.37.0.dist-info/METADATA +0 -1455
- {django_unfold-0.37.0.dist-info → django_unfold-0.39.0.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.37.0.dist-info → django_unfold-0.39.0.dist-info}/WHEEL +0 -0
@@ -4,35 +4,35 @@
|
|
4
4
|
{% load getattribute from getattributes %}
|
5
5
|
|
6
6
|
<table id="change-history" class="border-gray-200 border-spacing-none border-separate mb-6 w-full lg:border lg:rounded-md lg:shadow-sm lg:dark:border-gray-800">
|
7
|
-
<thead class="hidden text-
|
7
|
+
<thead class="hidden text-font-important-light lg:table-header-group dark:text-font-important-dark">
|
8
8
|
<tr>
|
9
|
-
<th class="align-middle font-
|
9
|
+
<th class="align-middle font-semibold px-3 py-2 text-left ">
|
10
10
|
{% trans 'Object' %}
|
11
11
|
</th>
|
12
12
|
|
13
13
|
{% for column in history_list_display %}
|
14
|
-
<th class="align-middle font-
|
14
|
+
<th class="align-middle font-semibold px-3 py-2 text-left ">
|
15
15
|
{% trans column %}
|
16
16
|
</th>
|
17
17
|
{% endfor %}
|
18
18
|
|
19
|
-
<th class="align-middle font-
|
19
|
+
<th class="align-middle font-semibold px-3 py-2 text-left">
|
20
20
|
{% trans 'Date/time' %}
|
21
21
|
</th>
|
22
22
|
|
23
|
-
<th class="align-middle font-
|
23
|
+
<th class="align-middle font-semibold px-3 py-2 text-left">
|
24
24
|
{% trans 'Comment' %}
|
25
25
|
</th>
|
26
26
|
|
27
|
-
<th class="align-middle font-
|
27
|
+
<th class="align-middle font-semibold px-3 py-2 text-left">
|
28
28
|
{% trans 'Changed by' %}
|
29
29
|
</th>
|
30
30
|
|
31
|
-
<th class="align-middle font-
|
31
|
+
<th class="align-middle font-semibold px-3 py-2 text-left">
|
32
32
|
{% trans 'Change reason' %}
|
33
33
|
</th>
|
34
34
|
|
35
|
-
<th class="align-middle font-
|
35
|
+
<th class="align-middle font-semibold px-3 py-2 text-left">
|
36
36
|
{% trans 'Changes' %}
|
37
37
|
</th>
|
38
38
|
</tr>
|
@@ -16,7 +16,7 @@
|
|
16
16
|
</button>
|
17
17
|
{% endif %}
|
18
18
|
|
19
|
-
<a href="{{ history_url }}" class="border font-medium mr-auto px-3 py-2 rounded-md text-sm text-gray-500 text-center transition-all w-full hover:bg-gray-50 lg:block lg:w-auto dark:border-gray-700 dark:text-
|
19
|
+
<a href="{{ history_url }}" class="border font-medium mr-auto px-3 py-2 rounded-md text-sm text-gray-500 text-center transition-all w-full hover:bg-gray-50 lg:block lg:w-auto dark:border-gray-700 dark:text-font-default-dark dark:hover:text-gray-200 dark:hover:bg-gray-900">
|
20
20
|
{% trans 'Close' %}
|
21
21
|
</a>
|
22
22
|
</div>
|
unfold/forms.py
CHANGED
@@ -10,9 +10,13 @@ from django.contrib.admin.forms import (
|
|
10
10
|
from django.contrib.auth.forms import (
|
11
11
|
AdminPasswordChangeForm as BaseAdminPasswordChangeForm,
|
12
12
|
)
|
13
|
+
|
14
|
+
try:
|
15
|
+
from django.contrib.auth.forms import AdminUserCreationForm as BaseUserCreationForm
|
16
|
+
except ImportError:
|
17
|
+
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
|
13
18
|
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
|
14
19
|
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
|
15
|
-
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
|
16
20
|
from django.http import HttpRequest
|
17
21
|
from django.utils.safestring import mark_safe
|
18
22
|
from django.utils.translation import gettext_lazy as _
|
unfold/settings.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Any, Dict
|
2
|
+
|
1
3
|
from django.conf import settings
|
2
4
|
|
3
5
|
CONFIG_DEFAULTS = {
|
@@ -11,6 +13,14 @@ CONFIG_DEFAULTS = {
|
|
11
13
|
"SHOW_HISTORY": True,
|
12
14
|
"SHOW_VIEW_ON_SITE": True,
|
13
15
|
"COLORS": {
|
16
|
+
"font": {
|
17
|
+
"subtle-light": "107 114 128", # text-gray-500
|
18
|
+
"subtle-dark": "156 163 175", # text-gray-400
|
19
|
+
"default-light": "75 85 99", # text-gray-600
|
20
|
+
"default-dark": "209 213 219", # text-gray-300
|
21
|
+
"important-light": "17 24 39", # text-gray-900
|
22
|
+
"important-dark": "243 244 246", # text-gray-100
|
23
|
+
},
|
14
24
|
"primary": {
|
15
25
|
"50": "250 245 255",
|
16
26
|
"100": "243 232 255",
|
@@ -47,4 +57,19 @@ def get_config(settings_name=None):
|
|
47
57
|
if settings_name is None:
|
48
58
|
settings_name = "UNFOLD"
|
49
59
|
|
50
|
-
|
60
|
+
def merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
|
61
|
+
result = dict1.copy()
|
62
|
+
|
63
|
+
for key, value in dict2.items():
|
64
|
+
if (
|
65
|
+
key in result
|
66
|
+
and isinstance(result[key], dict)
|
67
|
+
and isinstance(value, dict)
|
68
|
+
):
|
69
|
+
result[key] = merge_dicts(result[key], value)
|
70
|
+
else:
|
71
|
+
result[key] = value
|
72
|
+
|
73
|
+
return result
|
74
|
+
|
75
|
+
return merge_dicts(CONFIG_DEFAULTS, getattr(settings, settings_name, {}))
|
@@ -0,0 +1,295 @@
|
|
1
|
+
/*global SelectBox, interpolate*/
|
2
|
+
// Handles related-objects functionality: lookup link for raw_id_fields
|
3
|
+
// and Add Another links.
|
4
|
+
"use strict";
|
5
|
+
{
|
6
|
+
const $ = django.jQuery;
|
7
|
+
let popupIndex = 0;
|
8
|
+
const relatedWindows = [];
|
9
|
+
|
10
|
+
function dismissChildPopups() {
|
11
|
+
relatedWindows.forEach(function (win) {
|
12
|
+
if (!win.closed) {
|
13
|
+
win.dismissChildPopups();
|
14
|
+
win.close();
|
15
|
+
}
|
16
|
+
});
|
17
|
+
}
|
18
|
+
|
19
|
+
function setPopupIndex() {
|
20
|
+
if (document.getElementsByName("_popup").length > 0) {
|
21
|
+
const index = window.name.lastIndexOf("__") + 2;
|
22
|
+
popupIndex = parseInt(window.name.substring(index));
|
23
|
+
} else {
|
24
|
+
popupIndex = 0;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
function addPopupIndex(name) {
|
29
|
+
return name + "__" + (popupIndex + 1);
|
30
|
+
}
|
31
|
+
|
32
|
+
function removePopupIndex(name) {
|
33
|
+
return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), "");
|
34
|
+
}
|
35
|
+
|
36
|
+
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
|
37
|
+
const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ""));
|
38
|
+
const href = new URL(triggeringLink.href);
|
39
|
+
if (add_popup) {
|
40
|
+
href.searchParams.set("_popup", 1);
|
41
|
+
}
|
42
|
+
const win = window.open(
|
43
|
+
href,
|
44
|
+
name,
|
45
|
+
"height=768,width=1024,resizable=yes,scrollbars=yes"
|
46
|
+
);
|
47
|
+
relatedWindows.push(win);
|
48
|
+
win.focus();
|
49
|
+
return false;
|
50
|
+
}
|
51
|
+
|
52
|
+
function showRelatedObjectLookupPopup(triggeringLink) {
|
53
|
+
return showAdminPopup(triggeringLink, /^lookup_/, true);
|
54
|
+
}
|
55
|
+
|
56
|
+
function dismissRelatedLookupPopup(win, chosenId) {
|
57
|
+
const name = removePopupIndex(win.name);
|
58
|
+
const elem = document.getElementById(name);
|
59
|
+
if (elem.classList.contains("vManyToManyRawIdAdminField") && elem.value) {
|
60
|
+
elem.value += "," + chosenId;
|
61
|
+
} else {
|
62
|
+
document.getElementById(name).value = chosenId;
|
63
|
+
}
|
64
|
+
const index = relatedWindows.indexOf(win);
|
65
|
+
if (index > -1) {
|
66
|
+
relatedWindows.splice(index, 1);
|
67
|
+
}
|
68
|
+
win.close();
|
69
|
+
}
|
70
|
+
|
71
|
+
function showRelatedObjectPopup(triggeringLink) {
|
72
|
+
return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
|
73
|
+
}
|
74
|
+
|
75
|
+
function updateRelatedObjectLinks(triggeringLink) {
|
76
|
+
const $this = $(triggeringLink);
|
77
|
+
const siblings = $this.nextAll(
|
78
|
+
".view-related, .change-related, .delete-related"
|
79
|
+
);
|
80
|
+
if (!siblings.length) {
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
const value = $this.val();
|
84
|
+
if (value) {
|
85
|
+
siblings.each(function () {
|
86
|
+
const elm = $(this);
|
87
|
+
elm.attr(
|
88
|
+
"href",
|
89
|
+
elm.attr("data-href-template").replace("__fk__", value)
|
90
|
+
);
|
91
|
+
elm.removeAttr("aria-disabled");
|
92
|
+
});
|
93
|
+
} else {
|
94
|
+
siblings.removeAttr("href");
|
95
|
+
siblings.attr("aria-disabled", true);
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
function updateRelatedSelectsOptions(
|
100
|
+
currentSelect,
|
101
|
+
win,
|
102
|
+
objId,
|
103
|
+
newRepr,
|
104
|
+
newId,
|
105
|
+
skipIds = []
|
106
|
+
) {
|
107
|
+
// After create/edit a model from the options next to the current
|
108
|
+
// select (+ or :pencil:) update ForeignKey PK of the rest of selects
|
109
|
+
// in the page.
|
110
|
+
|
111
|
+
const path = win.location.pathname;
|
112
|
+
// Extract the model from the popup url '.../<model>/add/' or
|
113
|
+
// '.../<model>/<id>/change/' depending the action (add or change).
|
114
|
+
const modelName = path.split("/")[path.split("/").length - (objId ? 4 : 3)];
|
115
|
+
// Select elements with a specific model reference and context of "available-source".
|
116
|
+
const selectsRelated = document.querySelectorAll(
|
117
|
+
`[data-model-ref="${modelName}"] [data-context="available-source"]`
|
118
|
+
);
|
119
|
+
|
120
|
+
selectsRelated.forEach(function (select) {
|
121
|
+
if (
|
122
|
+
currentSelect === select ||
|
123
|
+
(skipIds && skipIds.includes(select.id))
|
124
|
+
) {
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
|
128
|
+
let option = select.querySelector(`option[value="${objId}"]`);
|
129
|
+
|
130
|
+
if (!option) {
|
131
|
+
option = new Option(newRepr, newId);
|
132
|
+
select.options.add(option);
|
133
|
+
// Update SelectBox cache for related fields.
|
134
|
+
if (
|
135
|
+
window.SelectBox !== undefined &&
|
136
|
+
!SelectBox.cache[currentSelect.id]
|
137
|
+
) {
|
138
|
+
SelectBox.add_to_cache(select.id, option);
|
139
|
+
SelectBox.redisplay(select.id);
|
140
|
+
}
|
141
|
+
return;
|
142
|
+
}
|
143
|
+
|
144
|
+
option.textContent = newRepr;
|
145
|
+
option.value = newId;
|
146
|
+
});
|
147
|
+
}
|
148
|
+
|
149
|
+
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
150
|
+
const name = removePopupIndex(win.name);
|
151
|
+
const elem = document.getElementById(name);
|
152
|
+
if (elem) {
|
153
|
+
const elemName = elem.nodeName.toUpperCase();
|
154
|
+
if (elemName === "SELECT") {
|
155
|
+
elem.options[elem.options.length] = new Option(
|
156
|
+
newRepr,
|
157
|
+
newId,
|
158
|
+
true,
|
159
|
+
true
|
160
|
+
);
|
161
|
+
updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
|
162
|
+
} else if (elemName === "INPUT") {
|
163
|
+
if (
|
164
|
+
elem.classList.contains("vManyToManyRawIdAdminField") &&
|
165
|
+
elem.value
|
166
|
+
) {
|
167
|
+
elem.value += "," + newId;
|
168
|
+
} else {
|
169
|
+
elem.value = newId;
|
170
|
+
}
|
171
|
+
}
|
172
|
+
// Trigger a change event to update related links if required.
|
173
|
+
$(elem).trigger("change");
|
174
|
+
} else {
|
175
|
+
const toId = name + "_to";
|
176
|
+
const toElem = document.getElementById(toId);
|
177
|
+
const o = new Option(newRepr, newId);
|
178
|
+
SelectBox.add_to_cache(toId, o);
|
179
|
+
SelectBox.redisplay(toId);
|
180
|
+
if (toElem && toElem.nodeName.toUpperCase() === "SELECT") {
|
181
|
+
const skipIds = [name + "_from"];
|
182
|
+
updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds);
|
183
|
+
}
|
184
|
+
}
|
185
|
+
const index = relatedWindows.indexOf(win);
|
186
|
+
if (index > -1) {
|
187
|
+
relatedWindows.splice(index, 1);
|
188
|
+
}
|
189
|
+
win.close();
|
190
|
+
}
|
191
|
+
|
192
|
+
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
193
|
+
const id = removePopupIndex(win.name.replace(/^edit_/, ""));
|
194
|
+
const selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]);
|
195
|
+
const selects = $(selectsSelector);
|
196
|
+
selects
|
197
|
+
.find("option")
|
198
|
+
.each(function () {
|
199
|
+
if (this.value === objId) {
|
200
|
+
this.textContent = newRepr;
|
201
|
+
this.value = newId;
|
202
|
+
}
|
203
|
+
})
|
204
|
+
.trigger("change");
|
205
|
+
updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
|
206
|
+
selects
|
207
|
+
.next()
|
208
|
+
.find(".select2-selection__rendered")
|
209
|
+
.each(function () {
|
210
|
+
// The element can have a clear button as a child.
|
211
|
+
// Use the lastChild to modify only the displayed value.
|
212
|
+
this.lastChild.textContent = newRepr;
|
213
|
+
this.title = newRepr;
|
214
|
+
});
|
215
|
+
const index = relatedWindows.indexOf(win);
|
216
|
+
if (index > -1) {
|
217
|
+
relatedWindows.splice(index, 1);
|
218
|
+
}
|
219
|
+
win.close();
|
220
|
+
}
|
221
|
+
|
222
|
+
function dismissDeleteRelatedObjectPopup(win, objId) {
|
223
|
+
const id = removePopupIndex(win.name.replace(/^delete_/, ""));
|
224
|
+
const selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]);
|
225
|
+
const selects = $(selectsSelector);
|
226
|
+
selects
|
227
|
+
.find("option")
|
228
|
+
.each(function () {
|
229
|
+
if (this.value === objId) {
|
230
|
+
$(this).remove();
|
231
|
+
}
|
232
|
+
})
|
233
|
+
.trigger("change");
|
234
|
+
const index = relatedWindows.indexOf(win);
|
235
|
+
if (index > -1) {
|
236
|
+
relatedWindows.splice(index, 1);
|
237
|
+
}
|
238
|
+
win.close();
|
239
|
+
}
|
240
|
+
|
241
|
+
window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
|
242
|
+
window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
|
243
|
+
window.showRelatedObjectPopup = showRelatedObjectPopup;
|
244
|
+
window.updateRelatedObjectLinks = updateRelatedObjectLinks;
|
245
|
+
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
|
246
|
+
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
247
|
+
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
248
|
+
window.dismissChildPopups = dismissChildPopups;
|
249
|
+
|
250
|
+
// Kept for backward compatibility
|
251
|
+
window.showAddAnotherPopup = showRelatedObjectPopup;
|
252
|
+
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
253
|
+
|
254
|
+
window.addEventListener("unload", function (evt) {
|
255
|
+
window.dismissChildPopups();
|
256
|
+
});
|
257
|
+
|
258
|
+
$(document).ready(function () {
|
259
|
+
setPopupIndex();
|
260
|
+
$("a[data-popup-opener]").on("click", function (event) {
|
261
|
+
event.preventDefault();
|
262
|
+
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
|
263
|
+
});
|
264
|
+
$("body").on(
|
265
|
+
"click",
|
266
|
+
'.related-widget-wrapper-link[data-popup="yes"]',
|
267
|
+
function (e) {
|
268
|
+
e.preventDefault();
|
269
|
+
if (this.href) {
|
270
|
+
const event = $.Event("django:show-related", { href: this.href });
|
271
|
+
$(this).trigger(event);
|
272
|
+
if (!event.isDefaultPrevented()) {
|
273
|
+
showRelatedObjectPopup(this);
|
274
|
+
}
|
275
|
+
}
|
276
|
+
}
|
277
|
+
);
|
278
|
+
$("body").on("change", ".related-widget-wrapper select", function (e) {
|
279
|
+
const event = $.Event("django:update-related");
|
280
|
+
$(this).trigger(event);
|
281
|
+
if (!event.isDefaultPrevented()) {
|
282
|
+
updateRelatedObjectLinks(this);
|
283
|
+
}
|
284
|
+
});
|
285
|
+
$(".related-widget-wrapper select").trigger("change");
|
286
|
+
$("body").on("click", ".related-lookup", function (e) {
|
287
|
+
e.preventDefault();
|
288
|
+
const event = $.Event("django:lookup-related");
|
289
|
+
$(this).trigger(event);
|
290
|
+
if (!event.isDefaultPrevented()) {
|
291
|
+
showRelatedObjectLookupPopup(this);
|
292
|
+
}
|
293
|
+
});
|
294
|
+
});
|
295
|
+
}
|