django-admin-inline-controls 0.1.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.
Files changed (33) hide show
  1. django_admin_inline_controls/__init__.py +1 -0
  2. django_admin_inline_controls/actions.py +176 -0
  3. django_admin_inline_controls/apps.py +7 -0
  4. django_admin_inline_controls/checks.py +212 -0
  5. django_admin_inline_controls/contrib/__init__.py +4 -0
  6. django_admin_inline_controls/contrib/filters.py +69 -0
  7. django_admin_inline_controls/contrib/nested.py +77 -0
  8. django_admin_inline_controls/contrib/unfold.py +85 -0
  9. django_admin_inline_controls/controls.py +684 -0
  10. django_admin_inline_controls/locale/es/LC_MESSAGES/django.mo +0 -0
  11. django_admin_inline_controls/locale/es/LC_MESSAGES/django.po +232 -0
  12. django_admin_inline_controls/mixins.py +932 -0
  13. django_admin_inline_controls/py.typed +0 -0
  14. django_admin_inline_controls/row_actions.py +173 -0
  15. django_admin_inline_controls/static/django_admin_inline_controls/css/contrib/unfold.css +111 -0
  16. django_admin_inline_controls/static/django_admin_inline_controls/css/core.css +294 -0
  17. django_admin_inline_controls/static/django_admin_inline_controls/js/actions.js +330 -0
  18. django_admin_inline_controls/static/django_admin_inline_controls/js/contrib/unfold.js +21 -0
  19. django_admin_inline_controls/static/django_admin_inline_controls/js/core.js +570 -0
  20. django_admin_inline_controls/static/django_admin_inline_controls/js/save.js +109 -0
  21. django_admin_inline_controls/templates/django_admin_inline_controls/footer.html +99 -0
  22. django_admin_inline_controls/templates/django_admin_inline_controls/inline.html +34 -0
  23. django_admin_inline_controls/templates/django_admin_inline_controls/inline_response.html +14 -0
  24. django_admin_inline_controls/templates/django_admin_inline_controls/row_actions.html +19 -0
  25. django_admin_inline_controls/templates/django_admin_inline_controls/tfoot.html +36 -0
  26. django_admin_inline_controls/templates/django_admin_inline_controls/toolbar.html +93 -0
  27. django_admin_inline_controls/templatetags/__init__.py +0 -0
  28. django_admin_inline_controls/templatetags/inline_controls.py +72 -0
  29. django_admin_inline_controls/types.py +40 -0
  30. django_admin_inline_controls-0.1.0.dist-info/METADATA +823 -0
  31. django_admin_inline_controls-0.1.0.dist-info/RECORD +33 -0
  32. django_admin_inline_controls-0.1.0.dist-info/WHEEL +4 -0
  33. django_admin_inline_controls-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,330 @@
1
+ /*
2
+ * django-admin-inline-controls: bulk actions (inline_bulk_actions) and row
3
+ * actions (inline_actions). Loaded after core.js, only by inlines that
4
+ * use them.
5
+ */
6
+ (() => {
7
+ const core = globalThis.DjangoAdminInlineControls;
8
+ const SCOPE = ".inline-controls-actions";
9
+ const ROW_SCOPE = ".inline-controls-row-actions-status";
10
+
11
+ /** Whether the inline has bulk actions (the toolbar's select). */
12
+ const hasBulkActions = (config) =>
13
+ Boolean(config.actionUrl) && (config.bulkActions ?? []).length > 0;
14
+
15
+ const format = (template, values) =>
16
+ template.replace(/%\((\w+)\)s/g, (_, key) => values[key]);
17
+
18
+ /** Saved rows of the inline with their primary keys. */
19
+ function selectableRows(root, config) {
20
+ const group = root.querySelector(`[id="${config.prefix}-group"]`);
21
+ if (!group) {
22
+ return [];
23
+ }
24
+ return core
25
+ .formRows(group, config.prefix, config)
26
+ .filter((row) => core.isSaved(row, config))
27
+ .map((row) => {
28
+ const index = core.formIndex(row, config.prefix);
29
+ const input = row.querySelector(
30
+ `[name="${config.prefix}-${index}-${config.pkName}"]`,
31
+ );
32
+ return { row, pk: input?.value };
33
+ })
34
+ .filter(({ pk }) => pk);
35
+ }
36
+
37
+ function addRowCheckbox(row, pk, config) {
38
+ if (row.querySelector(".inline-controls-select")) {
39
+ return;
40
+ }
41
+ const checkbox = document.createElement("input");
42
+ checkbox.type = "checkbox";
43
+ checkbox.className = "inline-controls-select";
44
+ checkbox.value = pk;
45
+ checkbox.setAttribute("form", config.actionsFormId);
46
+ checkbox.setAttribute("aria-label", config.messages.selectRow ?? "");
47
+ // Next to the object's name: the label line of a tabular row (inside
48
+ // the zero-width "original" cell) or the heading of a stacked one.
49
+ (core.query(row, config, "row_label") ?? row).prepend(checkbox);
50
+ }
51
+
52
+ function selectedPks(root) {
53
+ return [
54
+ ...root.querySelectorAll(".inline-controls-select:checked"),
55
+ ].map((checkbox) => checkbox.value);
56
+ }
57
+
58
+ function updateSelection(root, state) {
59
+ const { config } = state;
60
+ const boxes = [...root.querySelectorAll(".inline-controls-select")];
61
+ const checked = boxes.filter((box) => box.checked).length;
62
+ const total = config.totalCount ?? boxes.length;
63
+ const all = boxes.length > 0 && checked === boxes.length;
64
+ if (!all) {
65
+ state.selectAcross = false;
66
+ }
67
+ const toggle = root.querySelector(".inline-controls-select-all");
68
+ if (toggle) {
69
+ toggle.checked = all;
70
+ toggle.indeterminate = checked > 0 && !all;
71
+ }
72
+ const label = root.querySelector("[data-inline-controls-selection]");
73
+ if (label) {
74
+ label.textContent = state.selectAcross
75
+ ? format(config.messages.allSelected, { total })
76
+ : format(config.messages.selected, {
77
+ sel: checked,
78
+ cnt: total,
79
+ });
80
+ }
81
+ const across = root.querySelector(
82
+ "[data-inline-controls-select-across]",
83
+ );
84
+ if (across) {
85
+ across.hidden = !all || state.selectAcross || total <= boxes.length;
86
+ across.textContent = format(config.messages.selectAll, { total });
87
+ }
88
+ }
89
+
90
+ /** Show the row action buttons, rendered hidden by the server. */
91
+ function showRowActions(scope, config) {
92
+ if (!config.actionUrl || !config.rowActions) {
93
+ return;
94
+ }
95
+ for (const el of scope.querySelectorAll(
96
+ "[data-inline-controls-row-actions]",
97
+ )) {
98
+ el.hidden = false;
99
+ }
100
+ }
101
+
102
+ function setupActions(root, state) {
103
+ const { config } = state;
104
+ state.selectAcross = false;
105
+ showRowActions(root, config);
106
+ if (!hasBulkActions(config)) {
107
+ return;
108
+ }
109
+ for (const { row, pk } of selectableRows(root, config)) {
110
+ addRowCheckbox(row, pk, config);
111
+ }
112
+ const toggle = document.createElement("input");
113
+ toggle.type = "checkbox";
114
+ toggle.className = "inline-controls-select-all";
115
+ toggle.setAttribute("form", config.actionsFormId);
116
+ toggle.setAttribute("aria-label", config.messages.selectAllRows ?? "");
117
+ root.querySelector(".inline-controls-actions")?.prepend(toggle);
118
+ updateSelection(root, state);
119
+ }
120
+
121
+ async function runAction(root) {
122
+ const state = core.stateOf(root);
123
+ const { config } = state;
124
+ const select = root.querySelector("[data-inline-controls-action]");
125
+ const group = document.getElementById(`${config.prefix}-group`);
126
+ const form = group?.closest("form");
127
+ if (state.loading || !config.actionUrl || !select || !form) {
128
+ return;
129
+ }
130
+ const action = config.bulkActions.find(
131
+ (item) => item.name === select.value,
132
+ );
133
+ const pks = selectedPks(root);
134
+ if (!action) {
135
+ core.showStatus(root, "error", config.messages.noAction, SCOPE);
136
+ return;
137
+ }
138
+ if (pks.length === 0 && !state.selectAcross) {
139
+ core.showStatus(root, "error", config.messages.noSelection, SCOPE);
140
+ return;
141
+ }
142
+ const count = state.selectAcross
143
+ ? (config.totalCount ?? pks.length)
144
+ : pks.length;
145
+ if (
146
+ action.confirmation &&
147
+ !window.confirm(format(action.confirmation, { count }))
148
+ ) {
149
+ return;
150
+ }
151
+ if (state.dirty && !window.confirm(config.messages.unsaved)) {
152
+ return;
153
+ }
154
+ const data = actionData(root, form, config);
155
+ data.append("action", action.name);
156
+ for (const pk of pks) {
157
+ data.append("_selected_action", pk);
158
+ }
159
+ data.append("select_across", state.selectAcross ? "1" : "0");
160
+ await postAction(root, data, SCOPE);
161
+ }
162
+
163
+ /** One row's action: the button's row only, no selection needed. */
164
+ async function runRowAction(root, button) {
165
+ const state = core.stateOf(root);
166
+ const { config } = state;
167
+ const group = document.getElementById(`${config.prefix}-group`);
168
+ const form = group?.closest("form");
169
+ if (state.loading || !config.actionUrl || !form) {
170
+ return;
171
+ }
172
+ const { confirmation } = button.dataset;
173
+ if (confirmation && !window.confirm(confirmation)) {
174
+ return;
175
+ }
176
+ if (state.dirty && !window.confirm(config.messages.unsaved)) {
177
+ return;
178
+ }
179
+ const data = actionData(root, form, config);
180
+ data.append("row_action", button.dataset.inlineControlsRowAction);
181
+ data.append("_selected_action", button.dataset.pk);
182
+ await postAction(root, data, ROW_SCOPE);
183
+ }
184
+
185
+ /** The CSRF token and the number of rows loaded (infinite mode). */
186
+ function actionData(root, form, config) {
187
+ const data = new FormData();
188
+ const csrf = form.querySelector("[name=csrfmiddlewaretoken]");
189
+ if (csrf) {
190
+ data.append(csrf.name, csrf.value);
191
+ }
192
+ data.append(
193
+ "_inline_controls_loaded",
194
+ String(selectableRows(root, config).length),
195
+ );
196
+ return data;
197
+ }
198
+
199
+ /** POST an action and show its result (a download, a redirect, another
200
+ * page, or the re-rendered inline with its status under `scope`). */
201
+ async function postAction(root, data, scope) {
202
+ const state = core.stateOf(root);
203
+ const { config } = state;
204
+ state.loading = true;
205
+ root.classList.add("inline-controls-loading");
206
+ const target = new URL(
207
+ config.actionUrl + window.location.search,
208
+ window.location.href,
209
+ ).toString();
210
+ try {
211
+ const response = await fetch(target, {
212
+ method: "POST",
213
+ body: data,
214
+ credentials: "same-origin",
215
+ headers: { "X-Requested-With": "XMLHttpRequest" },
216
+ });
217
+ if (!response.ok) {
218
+ throw new Error(`HTTP ${response.status}`);
219
+ }
220
+ const disposition =
221
+ response.headers.get("Content-Disposition") ?? "";
222
+ if (disposition.includes("attachment")) {
223
+ const blob = await response.blob();
224
+ const link = document.createElement("a");
225
+ link.href = URL.createObjectURL(blob);
226
+ link.download =
227
+ disposition.match(/filename="?([^";]+)"?/)?.[1] ??
228
+ "download";
229
+ document.body.append(link);
230
+ link.click();
231
+ link.remove();
232
+ URL.revokeObjectURL(link.href);
233
+ state.loading = false;
234
+ root.classList.remove("inline-controls-loading");
235
+ return;
236
+ }
237
+ if (response.redirected) {
238
+ window.location.assign(response.url);
239
+ return;
240
+ }
241
+ const text = await response.text();
242
+ const doc = new DOMParser().parseFromString(text, "text/html");
243
+ const result = doc.getElementById(
244
+ `${config.prefix}-inline-controls-response`,
245
+ );
246
+ const fresh = doc.getElementById(root.id);
247
+ if (!result || !fresh) {
248
+ // Any other page the action returned (e.g. an intermediate
249
+ // step) replaces this one.
250
+ document.open();
251
+ document.write(text);
252
+ document.close();
253
+ return;
254
+ }
255
+ const node = core.swapInline(root, fresh);
256
+ core.setup(node);
257
+ core.reinitAdmin(node);
258
+ core.showStatus(
259
+ node,
260
+ result.dataset.status,
261
+ result.dataset.message,
262
+ scope,
263
+ );
264
+ } catch {
265
+ state.loading = false;
266
+ root.classList.remove("inline-controls-loading");
267
+ core.showStatus(
268
+ root,
269
+ "failed",
270
+ config.messages.actionFailed,
271
+ scope,
272
+ );
273
+ }
274
+ }
275
+
276
+ /** Tick every row's checkbox, or clear them. */
277
+ function checkAll(root, checked) {
278
+ for (const box of root.querySelectorAll(".inline-controls-select")) {
279
+ box.checked = checked;
280
+ }
281
+ }
282
+
283
+ core.register({
284
+ setup: setupActions,
285
+ rowsLoaded(root, state) {
286
+ showRowActions(root, state.config);
287
+ if (hasBulkActions(state.config)) {
288
+ for (const { row, pk } of selectableRows(root, state.config)) {
289
+ addRowCheckbox(row, pk, state.config);
290
+ }
291
+ updateSelection(root, state);
292
+ }
293
+ },
294
+ click(event, root, state) {
295
+ const rowButton = event.target.closest(
296
+ "[data-inline-controls-row-action]",
297
+ );
298
+ if (rowButton) {
299
+ runRowAction(root, rowButton);
300
+ return true;
301
+ }
302
+ if (event.target.closest("[data-inline-controls-run]")) {
303
+ runAction(root);
304
+ return true;
305
+ }
306
+ if (event.target.closest("[data-inline-controls-select-across]")) {
307
+ checkAll(root, true);
308
+ state.selectAcross = true;
309
+ updateSelection(root, state);
310
+ return true;
311
+ }
312
+ return false;
313
+ },
314
+ edit(event, root, state) {
315
+ const { target } = event;
316
+ if (target.getAttribute?.("form") !== state.config.actionsFormId) {
317
+ return false;
318
+ }
319
+ if (target.classList.contains("inline-controls-select-all")) {
320
+ checkAll(root, target.checked);
321
+ }
322
+ if (event.type === "change") {
323
+ updateSelection(root, state);
324
+ }
325
+ return true;
326
+ },
327
+ });
328
+
329
+ Object.assign(core, { runAction, runRowAction });
330
+ })();
@@ -0,0 +1,21 @@
1
+ /*
2
+ * django-admin-inline-controls: django-unfold integration.
3
+ *
4
+ * Unfold binds its "Add another" and delete buttons once, on page load:
5
+ * bind them on refreshed inlines and appended rows too.
6
+ */
7
+ document.addEventListener("inline-controls:updated", (event) => {
8
+ const handlers = {
9
+ ".add-row": window.addInlineTemplateHandler,
10
+ ".delete-template": window.deleteInlineTemplateHandler,
11
+ };
12
+ for (const [selector, handler] of Object.entries(handlers)) {
13
+ if (typeof handler !== "function") {
14
+ continue;
15
+ }
16
+ for (const el of event.target.querySelectorAll(selector)) {
17
+ // The same listener is only added once.
18
+ el.addEventListener("click", handler);
19
+ }
20
+ }
21
+ });