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,99 @@
1
+ {% load i18n %}
2
+ {% comment %}
3
+ Footer below the inline: pagination or infinite-scroll status and the
4
+ save-inline button. Override per inline with
5
+ `inline_controls_footer_template`. The JS moves this element to the end
6
+ of the inline and relies on the data-inline-controls-* attributes.
7
+ {% endcomment %}
8
+ {% block footer %}
9
+ {% if controls.has_footer %}
10
+ <div class="inline-controls-footer{% if controls.mode == 'infinite' %} inline-controls-infinite{% endif %}{% block footer_classes %}{% endblock %}">
11
+ {% block footer_start %}{% endblock %}
12
+
13
+ {% block footer_rows %}
14
+ {% if controls.summary_rows %}
15
+ <div class="inline-controls-footer-rows" data-inline-controls-footer-rows>
16
+ {% for row in controls.summary_rows %}
17
+ {% block footer_row %}
18
+ <div class="inline-controls-footer-row" data-label="{{ row.label }}">
19
+ <span class="inline-controls-footer-label">{{ row.label }}:</span>
20
+ {% for cell in row.cells %}
21
+ {% block footer_cell %}
22
+ <span class="inline-controls-footer-cell" data-column="{{ cell.column }}" data-footer-key="{{ forloop.parentloop.counter0 }}:{{ cell.column }}" data-footer-row="{{ row.label }}" data-value="{{ cell.raw }}">
23
+ <span class="inline-controls-footer-column">{{ cell.column_label }}</span>
24
+ <span class="inline-controls-footer-value">{{ cell.value }}</span>
25
+ </span>
26
+ {% endblock footer_cell %}
27
+ {% endfor %}
28
+ </div>
29
+ {% endblock footer_row %}
30
+ {% endfor %}
31
+ </div>
32
+ {% endif %}
33
+ {% endblock footer_rows %}
34
+
35
+ {% block pagination %}
36
+ {% if controls.paginator %}
37
+ {% if controls.mode == "infinite" %}
38
+ {% block infinite %}
39
+ {% block infinite_count %}
40
+ <span class="inline-controls-count">
41
+ {% blocktranslate with loaded=controls.loaded_count|default:0 total=controls.total_count %}Showing {{ loaded }} of {{ total }}{% endblocktranslate %}
42
+ </span>
43
+ {% endblock infinite_count %}
44
+ {% block load_more %}
45
+ {% if controls.next_page_url %}
46
+ <a href="{{ controls.next_page_url }}" class="inline-controls-load-more" data-inline-controls-more>{% block load_more_label %}{% translate "Load more" %}{% endblock %}</a>
47
+ {% endif %}
48
+ {% endblock load_more %}
49
+ {% endblock infinite %}
50
+ {% else %}
51
+ {% block page_links %}
52
+ <nav class="inline-controls-pagination" aria-label="{% translate 'Pagination' %}">
53
+ {% if controls.is_paginated %}
54
+ {% for link in controls.page_links %}
55
+ {% block page_link %}
56
+ {% if link.url is None %}
57
+ <span class="inline-controls-ellipsis">{{ link.number }}</span>
58
+ {% elif link.current %}
59
+ <span class="inline-controls-page inline-controls-current" aria-current="page">{{ link.number }}</span>
60
+ {% else %}
61
+ <a href="{{ link.url }}" class="inline-controls-page" data-inline-controls-nav>{{ link.number }}</a>
62
+ {% endif %}
63
+ {% endblock page_link %}
64
+ {% endfor %}
65
+ {% endif %}
66
+ </nav>
67
+ {% endblock page_links %}
68
+ {% block result_count %}
69
+ <span class="inline-controls-count">
70
+ {% blocktranslate count counter=controls.total_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktranslate %}
71
+ </span>
72
+ {% endblock result_count %}
73
+ {% endif %}
74
+ {% endif %}
75
+ {% endblock pagination %}
76
+
77
+ {% block save %}
78
+ {% if controls.save_url %}
79
+ <div class="inline-controls-save">
80
+ {% block save_status %}
81
+ <span class="inline-controls-status" role="status" aria-live="polite"></span>
82
+ {% endblock save_status %}
83
+ {% block save_button %}
84
+ <button type="button" class="button default" data-inline-controls-save title="{% translate 'Saves only this inline. Other changes on the page are kept but not saved.' %}">{% block save_label %}{{ controls.save_label }}{% endblock %}</button>
85
+ {% endblock save_button %}
86
+ </div>
87
+ {% endif %}
88
+ {% endblock save %}
89
+
90
+ {% block row_actions_status %}
91
+ {% if controls.has_row_actions %}
92
+ <span class="inline-controls-row-actions-status"><span class="inline-controls-status" role="status" aria-live="polite"></span></span>
93
+ {% endif %}
94
+ {% endblock row_actions_status %}
95
+
96
+ {% block footer_end %}{% endblock %}
97
+ </div>
98
+ {% endif %}
99
+ {% endblock footer %}
@@ -0,0 +1,34 @@
1
+ {% comment %}
2
+ Wrapper around the inline's own template. Override per inline with
3
+ `inline_controls_template` (e.g. a template that extends this one).
4
+ The JS relies on the id, the data-inline-controls attribute and the
5
+ inline-controls-* classes: keep them when overriding blocks.
6
+ {% endcomment %}
7
+ {% load inline_controls %}
8
+ {% with controls=inline_admin_formset.formset.inline_controls opts=inline_admin_formset.opts %}
9
+ {% if controls %}
10
+ {% block container %}
11
+ <div class="inline-controls{% block container_classes %}{% endblock %}" id="{{ controls.container_id }}" data-inline-controls="{{ controls.config_json }}"{% block container_attrs %}{% endblock %}>
12
+ {% block before_toolbar %}{% endblock %}
13
+ {% block toolbar %}
14
+ {% include opts.inline_controls_toolbar_template %}
15
+ {% endblock toolbar %}
16
+ {% block before_inline %}{% endblock %}
17
+ {% block inline %}
18
+ {# The inline's own template, with the footer rows as its <tfoot>. #}
19
+ {% inline_controls_render_inline opts.inline_controls_inner_template %}
20
+ {% endblock inline %}
21
+ {% block after_inline %}{% endblock %}
22
+ {% block footer %}
23
+ {% include opts.inline_controls_footer_template %}
24
+ {% endblock footer %}
25
+ {% block after_footer %}{% endblock %}
26
+ </div>
27
+ {% endblock container %}
28
+ {% else %}
29
+ {% block inline_without_controls %}
30
+ {# The add view: the footer rows' <tfoot> has no values yet. #}
31
+ {% inline_controls_render_inline opts.inline_controls_inner_template %}
32
+ {% endblock inline_without_controls %}
33
+ {% endif %}
34
+ {% endwith %}
@@ -0,0 +1,14 @@
1
+ {% comment %}
2
+ Response of the save-inline and action endpoints: the re-rendered inline
3
+ plus its status. The JS reads the id and the data-status/data-message
4
+ attributes.
5
+ {% endcomment %}
6
+ {% block response %}
7
+ <div id="{{ prefix }}-inline-controls-response" data-status="{{ status }}" data-message="{{ message }}">
8
+ {% block inlines %}
9
+ {% for inline_admin_formset in inline_admin_formsets %}
10
+ {% include inline_admin_formset.opts.template %}
11
+ {% endfor %}
12
+ {% endblock inlines %}
13
+ </div>
14
+ {% endblock response %}
@@ -0,0 +1,19 @@
1
+ {% comment %}
2
+ The action buttons of one saved row (`obj`), in the inline's "Actions"
3
+ column. Each action has `name`, `label`, `css_classes`, `attrs`,
4
+ `confirmation` and, for "view", `url`. The JS relies on the
5
+ data-inline-controls-row-action* attributes. Shown once the JS is ready.
6
+ {% endcomment %}
7
+ {% block row_actions %}
8
+ <span class="inline-controls-row-actions" data-inline-controls-row-actions hidden>
9
+ {% for action in actions %}
10
+ {% block row_action %}
11
+ {% if action.url %}
12
+ <a href="{{ action.url }}" role="button" class="button inline-controls-row-action {{ action.css_classes }}" data-inline-controls-row-link="{{ action.name }}" {{ action.attrs }}>{{ action.label }}</a>
13
+ {% else %}
14
+ <button type="button" class="button inline-controls-row-action {{ action.css_classes }}" data-inline-controls-row-action="{{ action.name }}" data-pk="{{ obj.pk }}"{% if action.confirmation %} data-confirmation="{{ action.confirmation }}"{% endif %} {{ action.attrs }}>{{ action.label }}</button>
15
+ {% endif %}
16
+ {% endblock row_action %}
17
+ {% endfor %}
18
+ </span>
19
+ {% endblock row_actions %}
@@ -0,0 +1,36 @@
1
+ {% comment %}
2
+ The footer rows as the <tfoot> of a tabular inline's table, one cell per
3
+ column. Rendered by the server, no JS. Override per inline with
4
+ `inline_footer_tfoot_template` (e.g. a template that extends this one);
5
+ format the values with `format_inline_footer_value()`.
6
+ data-footer-key / data-footer-row / data-column / data-value (the number,
7
+ unformatted) are there for the page's own JS.
8
+ {% endcomment %}
9
+ {% block tfoot %}
10
+ <tfoot class="inline-controls-tfoot">
11
+ {% for row in tfoot_rows %}
12
+ {% block tfoot_row %}
13
+ <tr class="inline-controls-tfoot-row" data-footer-row="{{ row.label }}">
14
+ {% if row.label_colspan %}
15
+ <th scope="row" colspan="{{ row.label_colspan }}">
16
+ {% block tfoot_label %}<span class="inline-controls-footer-label">{{ row.label }}</span>{% endblock %}
17
+ </th>
18
+ {% endif %}
19
+ {% for slot in row.cells %}
20
+ {% block tfoot_cell %}
21
+ {% if slot.cell %}
22
+ <td class="inline-controls-footer-value field-{{ slot.cell.column }}">
23
+ {% if slot.label_here %}<span class="inline-controls-footer-label">{{ row.label }}</span>{% endif %}
24
+ <span class="inline-controls-footer-number" data-footer-key="{{ slot.key }}" data-footer-row="{{ row.label }}" data-column="{{ slot.cell.column }}" data-value="{{ slot.cell.raw }}">{% block tfoot_value %}{{ slot.cell.value }}{% endblock %}</span>
25
+ </td>
26
+ {% else %}
27
+ <td></td>
28
+ {% endif %}
29
+ {% endblock tfoot_cell %}
30
+ {% endfor %}
31
+ {% if row.trailing %}<td colspan="{{ row.trailing }}"></td>{% endif %}
32
+ </tr>
33
+ {% endblock tfoot_row %}
34
+ {% endfor %}
35
+ </tfoot>
36
+ {% endblock tfoot %}
@@ -0,0 +1,93 @@
1
+ {% load i18n %}
2
+ {% comment %}
3
+ Toolbar above the inline: actions, filters and (for stacked inlines, or
4
+ columns without a header) sort links. Override per inline with
5
+ `inline_controls_toolbar_template`. The JS moves this element under the
6
+ inline's heading and relies on the data-inline-controls-* attributes.
7
+ {% endcomment %}
8
+ {% block toolbar %}
9
+ {% if controls.has_toolbar %}
10
+ <div class="inline-controls-toolbar{% block toolbar_classes %}{% endblock %}">
11
+ {% block toolbar_start %}{% endblock %}
12
+
13
+ {% block actions %}
14
+ {% if controls.bulk_actions %}
15
+ <div class="inline-controls-actions">
16
+ {% block action_menu %}
17
+ <label>
18
+ {% block action_label %}{% translate "Action:" %}{% endblock %}
19
+ <select name="action" form="{{ controls.actions_form_id }}" data-inline-controls-action>
20
+ {% block action_empty_option %}<option value="">---------</option>{% endblock %}
21
+ {% for action in controls.bulk_actions %}
22
+ {% block action_option %}<option value="{{ action.name }}">{{ action.description }}</option>{% endblock %}
23
+ {% endfor %}
24
+ </select>
25
+ </label>
26
+ {% endblock action_menu %}
27
+ {% block action_button %}
28
+ <button type="button" class="button" data-inline-controls-run title="{% translate 'Run the selected action' %}">{% block action_button_label %}{% translate "Go" %}{% endblock %}</button>
29
+ {% endblock action_button %}
30
+ {% block action_selection %}
31
+ <span class="inline-controls-selection" data-inline-controls-selection></span>
32
+ <a href="#" class="inline-controls-select-across" data-inline-controls-select-across hidden></a>
33
+ {% endblock action_selection %}
34
+ {% block action_status %}
35
+ <span class="inline-controls-status" role="status" aria-live="polite"></span>
36
+ {% endblock action_status %}
37
+ </div>
38
+ {% endif %}
39
+ {% endblock actions %}
40
+
41
+ {% block filters %}
42
+ {% if controls.filter_form %}
43
+ <div class="inline-controls-filters" role="search">
44
+ {% block filter_fields %}
45
+ {% for field in controls.filter_form %}
46
+ {% block filter_field %}
47
+ <div class="inline-controls-filter{% if field.errors %} errors{% endif %}">
48
+ {{ field.label_tag }}
49
+ {{ field }}
50
+ {{ field.errors }}
51
+ </div>
52
+ {% endblock filter_field %}
53
+ {% endfor %}
54
+ {% endblock filter_fields %}
55
+ {% block filter_buttons %}
56
+ <div class="inline-controls-filter-buttons">
57
+ {% block filter_apply_button %}
58
+ <button type="button" class="button" data-inline-controls-apply>{% block filter_apply_label %}{% translate "Filter" %}{% endblock %}</button>
59
+ {% endblock filter_apply_button %}
60
+ {% block filter_clear_button %}
61
+ {% if controls.is_filtered %}
62
+ <button type="button" class="button" data-inline-controls-clear>{% block filter_clear_label %}{% translate "Clear" %}{% endblock %}</button>
63
+ {% endif %}
64
+ {% endblock filter_clear_button %}
65
+ </div>
66
+ {% endblock filter_buttons %}
67
+ </div>
68
+ {% endif %}
69
+ {% endblock filters %}
70
+
71
+ {% block ordering %}
72
+ {% if controls.ordering_columns %}
73
+ <div class="inline-controls-ordering">
74
+ {% block ordering_label %}<span class="inline-controls-ordering-label">{% translate "Sort by:" %}</span>{% endblock %}
75
+ {% for column in controls.ordering_columns %}
76
+ {% block ordering_column %}
77
+ <span class="inline-controls-sort{% if column.direction %} inline-controls-{{ column.direction }}{% endif %}">
78
+ <a href="{{ column.toggle_url }}" class="inline-controls-sort-toggle" data-inline-controls-nav title="{% translate 'Toggle sorting' %}">{{ column.label }}</a>
79
+ {% if column.priority %}
80
+ <span class="inline-controls-sort-priority">{{ column.priority }}</span>
81
+ <a href="{{ column.remove_url }}" class="inline-controls-sort-remove" data-inline-controls-nav title="{% translate 'Remove from sorting' %}">&times;</a>
82
+ {% endif %}
83
+ </span>
84
+ {% endblock ordering_column %}
85
+ {% endfor %}
86
+ </div>
87
+ {% endif %}
88
+ {% endblock ordering %}
89
+
90
+ {% block toolbar_end %}{% endblock %}
91
+ </div>
92
+ {% endif %}
93
+ {% endblock toolbar %}
File without changes
@@ -0,0 +1,72 @@
1
+ """Server-side rendering of the footer rows inside the inline's own table."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from django import template
8
+ from django.utils.safestring import SafeString, mark_safe
9
+
10
+ from django_admin_inline_controls.controls import empty_footer_rows, layout_tfoot_rows
11
+
12
+ register = template.Library()
13
+
14
+
15
+ def _header_columns(html: str) -> tuple[dict[str, int], int]:
16
+ """Position of each field's column in the table's header row (its
17
+ ``th.column-<name>``), and the row's width, as rendered: the leading
18
+ columns (Django's narrow "original" one) and the trailing ones vary with
19
+ the Django version, the theme and the delete permission. Hidden headers
20
+ take no room."""
21
+ head = re.search(r"<thead.*?</thead>", html, re.S)
22
+ positions: dict[str, int] = {}
23
+ width = 0
24
+ for attrs in re.findall(r"<th\b([^>]*)>", head.group() if head else ""):
25
+ classes = re.search(r'class="([^"]*)"', attrs)
26
+ names = classes.group(1).split() if classes else []
27
+ if "hidden" in names:
28
+ continue
29
+ for name in names:
30
+ if name.startswith("column-"):
31
+ positions.setdefault(name.removeprefix("column-"), width)
32
+ span = re.search(r'colspan="(\d+)"', attrs)
33
+ width += int(span.group(1)) if span else 1
34
+ return positions, width
35
+
36
+
37
+ @register.simple_tag(takes_context=True)
38
+ def inline_controls_render_inline(
39
+ context: template.Context, template_name: str
40
+ ) -> SafeString:
41
+ """Render the inline's template (Django's, a theme's or your own) and, if
42
+ it has a table, insert the footer rows as its <tfoot> — no JS, and no
43
+ copy of Django's template to keep in sync."""
44
+ engine = context.template.engine # type: ignore[union-attr]
45
+ html = engine.get_template(template_name).render(context)
46
+ controls = context.get("controls")
47
+ formset = context["inline_admin_formset"]
48
+ if not formset.opts.inline_footer_tfoot:
49
+ return mark_safe(html)
50
+ if controls:
51
+ footer_rows = controls.footer_rows
52
+ else:
53
+ # The add view: the rows' structure, without values.
54
+ request = getattr(formset.formset, "inline_controls_request", None)
55
+ footer_rows = empty_footer_rows(formset.opts, request)
56
+ # Last in the table: if it has its own <tfoot> (Unfold's "Add another"),
57
+ # that one stays the table's footer and these rows show above it.
58
+ position = html.rfind("</table>")
59
+ rows = (
60
+ layout_tfoot_rows(footer_rows, *_header_columns(html))
61
+ if footer_rows and position != -1
62
+ else None
63
+ )
64
+ if not rows:
65
+ return mark_safe(html)
66
+ with context.push(tfoot_rows=rows):
67
+ tfoot = engine.get_template(formset.opts.inline_footer_tfoot_template).render(
68
+ context
69
+ )
70
+ if controls:
71
+ controls.tfoot_rendered = True
72
+ return mark_safe(html[:position] + tfoot + html[position:])
@@ -0,0 +1,40 @@
1
+ """Type aliases used across the package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import TYPE_CHECKING, Any, TypeAlias
7
+
8
+ from django.db.models import QuerySet
9
+ from django.db.models.expressions import BaseExpression
10
+ from django.forms import Form
11
+
12
+ if TYPE_CHECKING:
13
+ from django_admin_inline_controls.mixins import InlineActionSpec
14
+
15
+ #: How a column is ordered: a field path, an ORM expression or an explicit
16
+ #: ``(ascending, descending)`` pair.
17
+ OrderingValue: TypeAlias = str | BaseExpression | tuple[Any, Any]
18
+ #: ``inline_ordering_fields``: column names, or ``{column: OrderingValue}``.
19
+ OrderingFields: TypeAlias = Sequence[str] | Mapping[str, OrderingValue]
20
+ #: The current ordering: ``(column, descending)`` pairs, primary first.
21
+ Ordering: TypeAlias = list[tuple[str, bool]]
22
+ #: What filtering returns: the bound filter form (if any) and the queryset.
23
+ FilterResult: TypeAlias = tuple[Form | None, QuerySet]
24
+ #: ``inline_controls_selectors``: CSS selector(s) per key, tried in order.
25
+ Selectors: TypeAlias = Mapping[str, str | Sequence[str]]
26
+ #: ``inline_footer_rows``: ``(label, {column: value})`` pairs, a value being
27
+ #: an aggregate (``Sum("pages")``), a ``callable(queryset)`` or a constant.
28
+ FooterRows: TypeAlias = Sequence[tuple[Any, Mapping[str, Any]]]
29
+ #: Actions available to a user, keyed by name.
30
+ InlineActions: TypeAlias = "dict[str, InlineActionSpec]"
31
+
32
+ __all__ = [
33
+ "FilterResult",
34
+ "FooterRows",
35
+ "InlineActions",
36
+ "Ordering",
37
+ "OrderingFields",
38
+ "OrderingValue",
39
+ "Selectors",
40
+ ]