django-fast-treenode 3.0.7__py3-none-any.whl → 3.0.8__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.
@@ -2,24 +2,70 @@
2
2
  {% load admin_list %}
3
3
  {% load i18n %}
4
4
  {% load treenode_admin %}
5
+ {% load static %}
5
6
 
6
- {% block object-tools-items %}
7
- {{ block.super }}
8
- {% if import_export_enabled %}
9
- <li><a href="import/" class="button">Import</a></li>
10
- <li><a href="export/" class="button">Export</a></li>
7
+ {% block content %}
8
+
9
+ {% block object-tools %}
10
+ {% if has_add_permission %}
11
+ <ul class="object-tools">
12
+ {% block object-tools-items %}
13
+ {{ block.super }}
14
+ <li><a href="import/" class="button">Import</a></li>
15
+ <li><a href="export/" class="button">Export</a></li>
16
+ {% endblock %}
17
+ </ul>
11
18
  {% endif %}
12
- {% endblock %}
19
+ {% endblock %}
20
+
21
+ <div id="changelist" class="module filtered">
22
+ <div class="changelist-form-container">
23
+
24
+ {# Search bar + expand/collapse buttons #}
25
+ {% block search %}
26
+ <div id="toolbar">
27
+ <form id="changelist-search" method="get" role="search">
28
+ <label for="searchbar">
29
+ <img src="{% static 'admin/img/search.svg' %}" alt="{% translate 'Search' %}">
30
+ </label>
31
+ <input type="text" size="40" name="q" value="{{ cl.query }}" id="searchbar">
32
+ <input type="submit" value="{% translate 'Search' %}">
33
+ <button type="button" class="button treenode-button treenode-expand-all">
34
+ {% trans "Expand All" %}
35
+ </button>
36
+ <button type="button" class="button treenode-button treenode-collapse-all">
37
+ {% trans "Collapse All" %}
38
+ </button>
39
+ </form>
40
+ </div>
41
+ {% endblock %}
42
+
43
+ {# Form with list and actions #}
44
+ <form id="changelist-form" method="post" {% if cl.formset or action_form %} enctype="multipart/form-data"{% endif %}>
45
+ {% csrf_token %}
13
46
 
14
- {% block result_list %}
15
- {% if action_form and actions_on_top and cl.show_admin_actions %}
16
- {% admin_actions %}
17
- {% endif %}
47
+ {% if action_form and actions_on_top and cl.show_admin_actions %}
48
+ {% admin_actions %}
49
+ {% endif %}
18
50
 
19
- {% tree_result_list cl %}
51
+ {% block result_list %}
52
+ {% tree_result_list cl %}
53
+ {% endblock %}
20
54
 
21
- {% if action_form and actions_on_bottom and cl.show_admin_actions %}
22
- {% admin_actions %}
23
- {% endif %}
55
+ {% if action_form and actions_on_bottom and cl.show_admin_actions %}
56
+ {% admin_actions %}
57
+ {% endif %}
58
+
59
+ {% block pagination %}
60
+ {{ block.super }}
61
+ {% endblock %}
62
+ </form>
63
+ </div>
64
+
65
+ {# 📎 Боковая панель фильтров #}
66
+ {% block filters %}
67
+ {{ block.super }}
68
+ {% endblock %}
69
+ </div>
70
+ {% endblock %}
24
71
 
25
- {% endblock %}
@@ -2,49 +2,45 @@
2
2
 
3
3
  <div class="results">
4
4
  <table id="result_list">
5
-
6
-
7
- <thead>
8
- <tr>
9
- {% for h in headers %}
10
- <th scope="col"{{ h.class_attrib|safe }}>
11
- {% if "action-checkbox-column" in h.class_attrib %}
12
- <div class="text">
13
- <span>
14
- <input type="checkbox" id="action-toggle" aria-label="{% translate 'Select all objects on this page for an action' %}">
15
- </span>
16
- </div>
17
- <div class="clear"></div>
18
- {% else %}
19
- {% if h.sortable %}
20
- <div class="sortoptions">
21
- {% if h.url_remove %}
22
- <a class="sortremove" href="{{ h.url_remove }}" title="{% translate 'Remove from sorting' %}"></a>
23
- {% endif %}
24
- {% if h.url_toggle %}
25
- <a href="{{ h.url_toggle }}" class="toggle {% if h.ascending %}ascending{% else %}descending{% endif %}" title="{% translate 'Toggle sorting' %}"></a>
26
- {% endif %}
27
- </div>
28
- {% else %}
29
- <div class="sortoptions"></div>
30
- {% endif %}
31
-
32
- <div class="text">
33
- {% if h.sortable %}
34
- <a href="{{ h.url_primary }}">{{ h.text|safe }}</a>
5
+ <thead>
6
+ <tr>
7
+ {% for h in headers %}
8
+ <th scope="col"{{ h.class_attrib|safe }}>
9
+ {% if "action-checkbox-column" in h.class_attrib %}
10
+ <div class="text">
11
+ <span>
12
+ <input type="checkbox" id="action-toggle" aria-label="{% translate 'Select all objects on this page for an action' %}">
13
+ </span>
14
+ </div>
15
+ <div class="clear"></div>
35
16
  {% else %}
36
- <a href="#">{{ h.text|safe }}</a>
17
+ {% if h.sortable %}
18
+ <div class="sortoptions">
19
+ {% if h.url_remove %}
20
+ <a class="sortremove" href="{{ h.url_remove }}" title="{% translate 'Remove from sorting' %}"></a>
21
+ {% endif %}
22
+ {% if h.url_toggle %}
23
+ <a href="{{ h.url_toggle }}" class="toggle {% if h.ascending %}ascending{% else %}descending{% endif %}" title="{% translate 'Toggle sorting' %}"></a>
24
+ {% endif %}
25
+ </div>
26
+ {% else %}
27
+ <div class="sortoptions"></div>
28
+ {% endif %}
29
+ <div class="text">
30
+ {% if h.sortable %}
31
+ <a href="{{ h.url_primary }}">{{ h.text|safe }}</a>
32
+ {% else %}
33
+ <a href="#">{{ h.text|safe }}</a>
34
+ {% endif %}
35
+ </div>
36
+ <div class="clear"></div>
37
37
  {% endif %}
38
- </div>
39
- <div class="clear"></div>
40
- {% endif %}
41
- </th>
42
- {% endfor %}
43
- </tr>
44
- </thead>
38
+ </th>
39
+ {% endfor %}
40
+ </tr>
41
+ </thead>
45
42
 
46
-
47
- <tbody>
43
+ <tbody id="treenode-table">
48
44
  {% for row in rows %}
49
45
  <tr {{ row.attrs|safe }}>
50
46
  {% for cell in row.cells %}
@@ -53,5 +49,6 @@
53
49
  </tr>
54
50
  {% endfor %}
55
51
  </tbody>
52
+
56
53
  </table>
57
54
  </div>
@@ -1,8 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- Custon tags for changelist tamplate.
3
+ Custom tags for changelist template.
4
4
 
5
- Version: 3.0.0
5
+ Version: 3.1.0
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
@@ -10,30 +10,73 @@ Email: timurkady@yandex.com
10
10
 
11
11
  from django import template
12
12
  from django.contrib.admin.templatetags import admin_list
13
+ from django.utils.html import format_html, mark_safe
13
14
 
14
15
  register = template.Library()
15
16
 
16
17
 
17
- @register.inclusion_tag(
18
- "treenode/admin/treenode_rows.html",
19
- takes_context=True,
20
- )
18
+ @register.inclusion_tag("treenode/admin/treenode_rows.html", takes_context=True)
21
19
  def tree_result_list(context, cl):
22
- """
23
- Return custom results for change_list.
24
-
25
- Заголовки берём из стандартного admin_list.result_headers(),
26
- а строки формируем как угодно.
27
- """
20
+ """Get result list."""
28
21
  headers = list(admin_list.result_headers(cl))
29
22
 
23
+ # Add a checkbox title manually if it is missing
24
+ if cl.actions and not any("action-checkbox-column" in h["class_attrib"] for h in headers):
25
+ headers.insert(0, {
26
+ "text": "",
27
+ "class_attrib": ' class="action-checkbox-column"',
28
+ "sortable": False
29
+ })
30
+
30
31
  rows = []
32
+
31
33
  for obj in cl.result_list:
32
34
  cells = list(admin_list.items_for_result(cl, obj, None))
35
+
36
+ # Insert checkbox manually
37
+ checkbox = format_html(
38
+ '<td class="action-checkbox">'
39
+ '<input type="checkbox" name="_selected_action" value="{}" class="action-select" /></td>',
40
+ obj.pk
41
+ )
42
+ cells.insert(0, checkbox)
43
+
44
+ # Replace the toggle cell (3rd after inserting checkbox and move)
45
+ is_leaf = getattr(obj, "is_leaf", lambda: True)()
46
+ toggle_html = format_html(
47
+ '<td class="field-toggle">{}</td>',
48
+ format_html(
49
+ '<button class="treenode-toggle" data-node-id="{}">►</button>',
50
+ obj.pk
51
+ ) if not is_leaf else mark_safe('<div class="treenode-space">&nbsp;</div>')
52
+ )
53
+ if len(cells) >= 3:
54
+ cells.pop(2)
55
+ cells.insert(2, toggle_html)
56
+
57
+ depth = getattr(obj, "get_depth", lambda: 0)()
58
+ parent_id = getattr(obj, "parent_id", "")
59
+ is_root = not parent_id
60
+
61
+ classes = ["treenode-row"]
62
+ if is_root:
63
+ classes.append("treenode-root")
64
+ else:
65
+ classes.append("treenode-hidden")
66
+
67
+ row_attrs = {
68
+ "class": " ".join(classes),
69
+ "data-node-id": obj.pk,
70
+ "data-parent-id": parent_id or "",
71
+ "data-depth": depth,
72
+ }
73
+
33
74
  rows.append({
34
- "attrs": getattr(obj, "row_attrs", ""),
75
+ "attrs": " ".join(f'{k}="{v}"' for k, v in row_attrs.items()),
35
76
  "cells": cells,
36
- "form": None, # поддержка list_editable, если нужно
77
+ "form": None,
78
+ "is_leaf": is_leaf,
79
+ "node_id": obj.pk,
37
80
  })
38
81
 
39
82
  return {
treenode/version.py CHANGED
@@ -4,9 +4,9 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 3.0.7
7
+ Version: 3.0.8
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
- __version__ = '3.0.7'
12
+ __version__ = '3.0.8'
treenode/views/autoapi.py CHANGED
@@ -1,91 +1,91 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Route generator for all models inherited from TreeNodeModel
4
-
5
- Version: 3.0.0
6
- Author: Timur Kady
7
- Email: timurkady@yandex.com
8
- """
9
-
10
-
11
- from django.apps import apps
12
- from django.urls import path
13
- from django.conf import settings
14
- from django.contrib.auth.decorators import login_required
15
-
16
- from ..models import TreeNodeModel
17
- from .autocomplete import TreeNodeAutocompleteView
18
- from .children import TreeChildrenView
19
- from .search import TreeSearchView
20
- from .crud import TreeNodeBaseAPIView
21
-
22
-
23
- class AutoTreeAPI:
24
- """Auto-discover and expose TreeNode-based APIs."""
25
-
26
- def __init__(self, base_view=TreeNodeBaseAPIView, base_url="api"):
27
- """Init auto-discover."""
28
- self.base_view = base_view
29
- self.base_url = base_url
30
-
31
- def protect_view(self, view, model):
32
- """
33
- Protect view.
34
-
35
- Protects view with login_required if needed, based on model attribute
36
- or global settings.
37
- """
38
- if getattr(model, 'api_login_required', None) is True:
39
- return login_required(view)
40
- if getattr(settings, 'TREENODE_API_LOGIN_REQUIRED', False):
41
- return login_required(view)
42
- return view
43
-
44
- def discover(self):
45
- """Scan models and generate API urls."""
46
- urls = [
47
- # Admin and Widget end-points
48
- path("widget/autocomplete/", TreeNodeAutocompleteView.as_view(), name="tree_autocomplete"), # noqa: D501
49
- path("widget/children/", TreeChildrenView.as_view(), name="tree_children"), # noqa: D501
50
- path("widget/search/", TreeSearchView.as_view(), name="tree_search"), # noqa: D501
51
- ]
52
- for model in apps.get_models():
53
- if issubclass(model, TreeNodeModel) and model is not TreeNodeModel:
54
- model_name = model._meta.model_name
55
-
56
- # Dynamically create an API view class for the model
57
- api_view_class = type(
58
- f"{model_name.capitalize()}APIView",
59
- (self.base_view,),
60
- {"model": model}
61
- )
62
-
63
- # List of API actions and their corresponding URL patterns
64
- action_patterns = [
65
- # List / Create
66
- ("", None, f"{model_name}-list"),
67
- # Retrieve / Update / Delete
68
- ("<int:pk>/", None, f"{model_name}-detail"),
69
- ("tree/", {'action': 'tree'}, f"{model_name}-tree"),
70
- # Direct children
71
- ("<int:pk>/children/", {'action': 'children'}, f"{model_name}-children"), # noqa: D501
72
- # All descendants
73
- ("<int:pk>/descendants/", {'action': 'descendants'}, f"{model_name}-descendants"), # noqa: D501
74
- # Ancestors + Self + Descendants
75
- ("<int:pk>/family/", {'action': 'family'}, f"{model_name}-family"), # noqa: D501
76
- ]
77
-
78
- # Create secured view instance once
79
- view = self.protect_view(api_view_class.as_view(), model)
80
-
81
- # Automatically build all paths for this model
82
- for url_suffix, extra_kwargs, route_name in action_patterns:
83
- urls.append(
84
- path(
85
- f"{self.base_url}/{model_name}/{url_suffix}",
86
- view,
87
- extra_kwargs or {},
88
- name=route_name
89
- )
90
- )
91
- return urls
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Route generator for all models inherited from TreeNodeModel
4
+
5
+ Version: 3.0.0
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+
11
+ from django.apps import apps
12
+ from django.urls import path
13
+ from django.conf import settings
14
+ from django.contrib.auth.decorators import login_required
15
+
16
+ from ..models import TreeNodeModel
17
+ from .autocomplete import TreeNodeAutocompleteView
18
+ from .children import TreeChildrenView
19
+ from .search import TreeSearchView
20
+ from .crud import TreeNodeBaseAPIView
21
+
22
+
23
+ class AutoTreeAPI:
24
+ """Auto-discover and expose TreeNode-based APIs."""
25
+
26
+ def __init__(self, base_view=TreeNodeBaseAPIView, base_url="api"):
27
+ """Init auto-discover."""
28
+ self.base_view = base_view
29
+ self.base_url = base_url
30
+
31
+ def protect_view(self, view, model):
32
+ """
33
+ Protect view.
34
+
35
+ Protects view with login_required if needed, based on model attribute
36
+ or global settings.
37
+ """
38
+ if getattr(model, 'api_login_required', None) is True:
39
+ return login_required(view)
40
+ if getattr(settings, 'TREENODE_API_LOGIN_REQUIRED', False):
41
+ return login_required(view)
42
+ return view
43
+
44
+ def discover(self):
45
+ """Scan models and generate API urls."""
46
+ urls = [
47
+ # Admin and Widget end-points
48
+ path("widget/autocomplete/", TreeNodeAutocompleteView.as_view(), name="tree_autocomplete"), # noqa: D501
49
+ path("widget/children/", TreeChildrenView.as_view(), name="tree_children"), # noqa: D501
50
+ path("widget/search/", TreeSearchView.as_view(), name="tree_search"), # noqa: D501
51
+ ]
52
+ for model in apps.get_models():
53
+ if issubclass(model, TreeNodeModel) and model is not TreeNodeModel:
54
+ model_name = model._meta.model_name
55
+
56
+ # Dynamically create an API view class for the model
57
+ api_view_class = type(
58
+ f"{model_name.capitalize()}APIView",
59
+ (self.base_view,),
60
+ {"model": model}
61
+ )
62
+
63
+ # List of API actions and their corresponding URL patterns
64
+ action_patterns = [
65
+ # List / Create
66
+ ("", None, f"{model_name}-list"),
67
+ # Retrieve / Update / Delete
68
+ ("<int:pk>/", None, f"{model_name}-detail"),
69
+ ("tree/", {'action': 'tree'}, f"{model_name}-tree"),
70
+ # Direct children
71
+ ("<int:pk>/children/", {'action': 'children'}, f"{model_name}-children"), # noqa: D501
72
+ # All descendants
73
+ ("<int:pk>/descendants/", {'action': 'descendants'}, f"{model_name}-descendants"), # noqa: D501
74
+ # Ancestors + Self + Descendants
75
+ ("<int:pk>/family/", {'action': 'family'}, f"{model_name}-family"), # noqa: D501
76
+ ]
77
+
78
+ # Create secured view instance once
79
+ view = self.protect_view(api_view_class.as_view(), model)
80
+
81
+ # Automatically build all paths for this model
82
+ for url_suffix, extra_kwargs, route_name in action_patterns:
83
+ urls.append(
84
+ path(
85
+ f"{self.base_url}/{model_name}/{url_suffix}",
86
+ view,
87
+ extra_kwargs or {},
88
+ name=route_name
89
+ )
90
+ )
91
+ return urls
@@ -1,52 +1,52 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
-
4
-
5
- Handles autocomplete suggestions for TreeNode models.
6
-
7
- Version: 3.0.0
8
- Author: Timur Kady
9
- Email: timurkady@yandex.com
10
- """
11
-
12
- # autocomplete.py
13
- from django.http import JsonResponse
14
- from django.views import View
15
- from django.contrib.admin.views.decorators import staff_member_required
16
- from django.utils.decorators import method_decorator
17
-
18
- from .common import get_model_from_request
19
-
20
-
21
- @method_decorator(staff_member_required, name='dispatch')
22
- class TreeNodeAutocompleteView(View):
23
- """Widget Autocomplete View."""
24
-
25
- def get(self, request, *args, **kwargs):
26
- """Get request."""
27
- select_id = request.GET.get("select_id", "").strip()
28
- q = request.GET.get("q", "").strip()
29
-
30
- model = get_model_from_request(request)
31
- results = []
32
-
33
- if q:
34
- field = getattr(model, "display_field", "id")
35
- queryset = model.objects.filter(**{f"{field}__icontains": q})
36
- elif select_id:
37
- pk = int(select_id)
38
- queryset = model.objects.filter(pk=pk)
39
- else:
40
- queryset = model.objects.filter(parent__isnull=True)
41
-
42
- results = [
43
- {
44
- "id": obj.pk,
45
- "text": str(obj),
46
- "level": obj.get_depth(),
47
- "is_leaf": obj.is_leaf(),
48
- }
49
- for obj in queryset[:20]
50
- ]
51
-
52
- return JsonResponse({"results": results})
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+
4
+
5
+ Handles autocomplete suggestions for TreeNode models.
6
+
7
+ Version: 3.0.0
8
+ Author: Timur Kady
9
+ Email: timurkady@yandex.com
10
+ """
11
+
12
+ # autocomplete.py
13
+ from django.http import JsonResponse
14
+ from django.views import View
15
+ from django.contrib.admin.views.decorators import staff_member_required
16
+ from django.utils.decorators import method_decorator
17
+
18
+ from .common import get_model_from_request
19
+
20
+
21
+ @method_decorator(staff_member_required, name='dispatch')
22
+ class TreeNodeAutocompleteView(View):
23
+ """Widget Autocomplete View."""
24
+
25
+ def get(self, request, *args, **kwargs):
26
+ """Get request."""
27
+ select_id = request.GET.get("select_id", "").strip()
28
+ q = request.GET.get("q", "").strip()
29
+
30
+ model = get_model_from_request(request)
31
+ results = []
32
+
33
+ if q:
34
+ field = getattr(model, "display_field", "id")
35
+ queryset = model.objects.filter(**{f"{field}__icontains": q})
36
+ elif select_id:
37
+ pk = int(select_id)
38
+ queryset = model.objects.filter(pk=pk)
39
+ else:
40
+ queryset = model.objects.filter(parent__isnull=True)
41
+
42
+ results = [
43
+ {
44
+ "id": obj.pk,
45
+ "text": str(obj),
46
+ "level": obj.get_depth(),
47
+ "is_leaf": obj.is_leaf(),
48
+ }
49
+ for obj in queryset[:20]
50
+ ]
51
+
52
+ return JsonResponse({"results": results})
@@ -1,41 +1,41 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
-
4
- Version: 3.0.0
5
- Author: Timur Kady
6
- Email: timurkady@yandex.com
7
- """
8
-
9
- from django.http import JsonResponse
10
- from django.views import View
11
- from django.contrib.admin.views.decorators import staff_member_required
12
- from django.utils.decorators import method_decorator
13
-
14
- from .common import get_model_from_request
15
-
16
-
17
- @method_decorator(staff_member_required, name='dispatch')
18
- class TreeChildrenView(View):
19
- def get(self, request, *args, **kwargs):
20
- model = get_model_from_request(request)
21
- reference_id = request.GET.get("reference_id")
22
- if not reference_id:
23
- return JsonResponse({"results": []})
24
-
25
- obj = model.objects.filter(pk=reference_id).first()
26
- if not obj or obj.is_leaf():
27
- return JsonResponse({"results": []})
28
-
29
- results = [
30
- {
31
- "id": node.pk,
32
- "text": str(node),
33
- "level": node.get_depth(),
34
- "is_leaf": node.is_leaf(),
35
- }
36
- for node in obj.get_children()
37
- ]
38
- return JsonResponse({"results": results})
39
-
40
-
41
- # The End
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+
4
+ Version: 3.0.0
5
+ Author: Timur Kady
6
+ Email: timurkady@yandex.com
7
+ """
8
+
9
+ from django.http import JsonResponse
10
+ from django.views import View
11
+ from django.contrib.admin.views.decorators import staff_member_required
12
+ from django.utils.decorators import method_decorator
13
+
14
+ from .common import get_model_from_request
15
+
16
+
17
+ @method_decorator(staff_member_required, name='dispatch')
18
+ class TreeChildrenView(View):
19
+ def get(self, request, *args, **kwargs):
20
+ model = get_model_from_request(request)
21
+ reference_id = request.GET.get("reference_id")
22
+ if not reference_id:
23
+ return JsonResponse({"results": []})
24
+
25
+ obj = model.objects.filter(pk=reference_id).first()
26
+ if not obj or obj.is_leaf():
27
+ return JsonResponse({"results": []})
28
+
29
+ results = [
30
+ {
31
+ "id": node.pk,
32
+ "text": str(node),
33
+ "level": node.get_depth(),
34
+ "is_leaf": node.is_leaf(),
35
+ }
36
+ for node in obj.get_children()
37
+ ]
38
+ return JsonResponse({"results": results})
39
+
40
+
41
+ # The End