django-fast-treenode 2.0.10__py3-none-any.whl → 2.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.
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
- django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/docs/.gitignore +0 -0
- treenode/docs/about.md +36 -0
- treenode/docs/admin.md +104 -0
- treenode/docs/api.md +739 -0
- treenode/docs/cache.md +187 -0
- treenode/docs/import_export.md +35 -0
- treenode/docs/index.md +30 -0
- treenode/docs/installation.md +74 -0
- treenode/docs/migration.md +145 -0
- treenode/docs/models.md +128 -0
- treenode/docs/roadmap.md +45 -0
- treenode/forms.py +33 -22
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +39 -65
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/tree_node_import.html +27 -9
- treenode/templates/admin/tree_node_import_report.html +32 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +63 -36
- treenode/utils/importer.py +168 -161
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +119 -38
- treenode/widgets.py +104 -40
- django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
- treenode/admin.py +0 -396
- treenode/docs/Documentation +0 -664
- treenode/managers.py +0 -281
- treenode/models/proxy.py +0 -650
- {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/views.py
CHANGED
@@ -14,33 +14,100 @@ Features:
|
|
14
14
|
- Uses optimized QuerySets for efficient database queries.
|
15
15
|
- Handles validation and error responses gracefully.
|
16
16
|
|
17
|
-
Version: 2.
|
17
|
+
Version: 2.1.0
|
18
18
|
Author: Timur Kady
|
19
19
|
Email: timurkady@yandex.com
|
20
20
|
"""
|
21
21
|
|
22
|
-
|
22
|
+
from django.apps import apps
|
23
23
|
from django.http import JsonResponse
|
24
|
+
from django.utils.translation import gettext_lazy as _
|
24
25
|
from django.views import View
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
|
27
|
+
import logging
|
28
|
+
|
29
|
+
logger = logging.getLogger(__name__)
|
28
30
|
|
29
31
|
|
30
32
|
class TreeNodeAutocompleteView(View):
|
31
|
-
"""
|
33
|
+
"""
|
34
|
+
Return JSON data for Select2 with tree structure.
|
35
|
+
|
36
|
+
Lazy load tree nodes for Select2 scrolling with reference node support.
|
37
|
+
"""
|
32
38
|
|
33
39
|
def get(self, request):
|
34
|
-
"""
|
40
|
+
"""
|
41
|
+
Process an AJAX request to lazily load tree nodes.
|
42
|
+
|
43
|
+
Operation logic:
|
44
|
+
"""
|
45
|
+
# Search Processing
|
35
46
|
q = request.GET.get("q", "")
|
36
|
-
|
47
|
+
if q:
|
48
|
+
return self.search(request)
|
37
49
|
|
38
|
-
|
50
|
+
# Model extracting
|
51
|
+
model_label = request.GET.get("model")
|
52
|
+
try:
|
53
|
+
model = apps.get_model(model_label)
|
54
|
+
except LookupError:
|
39
55
|
return JsonResponse(
|
40
|
-
{"error": "
|
56
|
+
{"error": f"Invalid model: {model_label}"},
|
41
57
|
status=400
|
42
58
|
)
|
43
59
|
|
60
|
+
# select_id not specified
|
61
|
+
queryset_list = model.get_roots()
|
62
|
+
|
63
|
+
select_id = request.GET.get("select_id", "")
|
64
|
+
if select_id:
|
65
|
+
select = model.objects.filter(pk=select_id).first()
|
66
|
+
if not select:
|
67
|
+
return JsonResponse(
|
68
|
+
{"error": f"Invalid select_id: {select_id}"},
|
69
|
+
status=400
|
70
|
+
)
|
71
|
+
breadcrumbs = select.get_breadcrumbs()
|
72
|
+
# Delete self
|
73
|
+
del breadcrumbs[-1]
|
74
|
+
for pk in breadcrumbs:
|
75
|
+
parent = model.model.objects.filter(pk=pk).first()
|
76
|
+
children = parent.get_children()
|
77
|
+
queryset_list.extend(children)
|
78
|
+
|
79
|
+
nodes = model._sort_node_list(queryset_list)
|
80
|
+
# Generate a response
|
81
|
+
results = [
|
82
|
+
{
|
83
|
+
"id": node.pk,
|
84
|
+
"text": str(node),
|
85
|
+
"level": node.get_depth(),
|
86
|
+
"is_leaf": node.is_leaf(),
|
87
|
+
}
|
88
|
+
for node in nodes
|
89
|
+
]
|
90
|
+
# Add the "Root" option to the top of the list
|
91
|
+
root_option = {
|
92
|
+
"id": "",
|
93
|
+
"text": _("Root"),
|
94
|
+
"level": 0,
|
95
|
+
"is_leaf": True,
|
96
|
+
}
|
97
|
+
results.insert(0, root_option)
|
98
|
+
|
99
|
+
response_data = {"results": results}
|
100
|
+
return JsonResponse(response_data)
|
101
|
+
|
102
|
+
def search(self, request):
|
103
|
+
"""Search processing."""
|
104
|
+
# Chack search query
|
105
|
+
q = request.GET.get("q", "")
|
106
|
+
if not q:
|
107
|
+
return JsonResponse({"results": []})
|
108
|
+
|
109
|
+
# Model extracting
|
110
|
+
model_label = request.GET.get("model")
|
44
111
|
try:
|
45
112
|
model = apps.get_model(model_label)
|
46
113
|
except LookupError:
|
@@ -49,21 +116,21 @@ class TreeNodeAutocompleteView(View):
|
|
49
116
|
status=400
|
50
117
|
)
|
51
118
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
for index, pk in enumerate(pk_list)],
|
58
|
-
default=Value(len(pk_list)),
|
59
|
-
output_field=IntegerField())
|
60
|
-
)[:10]
|
119
|
+
# Search
|
120
|
+
params = {}
|
121
|
+
treenode_field = model.treenode_display_field
|
122
|
+
if not treenode_field:
|
123
|
+
return {"results": ""}
|
61
124
|
|
125
|
+
params[f"{treenode_field}__icontains"] = q
|
126
|
+
queryset = model.objects.filter(**params)[:15]
|
127
|
+
queryset_list = list(queryset)
|
128
|
+
nodes = model._sort_node_list(queryset_list)
|
62
129
|
results = [
|
63
130
|
{
|
64
131
|
"id": node.pk,
|
65
132
|
"text": node.name,
|
66
|
-
"level": node.
|
133
|
+
"level": node.get_depth(),
|
67
134
|
"is_leaf": node.is_leaf(),
|
68
135
|
}
|
69
136
|
for node in nodes
|
@@ -71,17 +138,18 @@ class TreeNodeAutocompleteView(View):
|
|
71
138
|
return JsonResponse({"results": results})
|
72
139
|
|
73
140
|
|
74
|
-
class
|
75
|
-
"""Return
|
141
|
+
class ChildrenView(View):
|
142
|
+
"""Return JSON data for Select2 with node children."""
|
76
143
|
|
77
144
|
def get(self, request):
|
78
|
-
"""
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
145
|
+
"""Process an AJAX request to load node children."""
|
146
|
+
# Get reference_id
|
147
|
+
reference_id = request.GET.get("reference_id", "")
|
148
|
+
if not reference_id:
|
149
|
+
return JsonResponse({"results": []})
|
150
|
+
|
151
|
+
# Model extracting
|
152
|
+
model_label = request.GET.get("model")
|
85
153
|
try:
|
86
154
|
model = apps.get_model(model_label)
|
87
155
|
except LookupError:
|
@@ -89,15 +157,28 @@ class GetChildrenCountView(View):
|
|
89
157
|
{"error": f"Invalid model: {model_label}"},
|
90
158
|
status=400
|
91
159
|
)
|
92
|
-
|
93
|
-
|
94
|
-
parent_node = model.objects.get(pk=parent_id)
|
95
|
-
children_count = parent_node.get_children_count()
|
96
|
-
print("parent_id=", parent_id, " children_count=", children_count)
|
97
|
-
except ObjectDoesNotExist:
|
160
|
+
obj = model.objects.filter(pk=reference_id).first()
|
161
|
+
if not obj:
|
98
162
|
return JsonResponse(
|
99
|
-
{"error": "
|
100
|
-
status=
|
163
|
+
{"error": f"Invalid reference_id: {reference_id}"},
|
164
|
+
status=400
|
101
165
|
)
|
102
166
|
|
103
|
-
|
167
|
+
if obj.is_leaf():
|
168
|
+
return JsonResponse({"results": []})
|
169
|
+
|
170
|
+
queryset_list = obj.get_children()
|
171
|
+
nodes = model._sort_node_list(queryset_list)
|
172
|
+
results = [
|
173
|
+
{
|
174
|
+
"id": node.pk,
|
175
|
+
"text": node.name,
|
176
|
+
"level": node.get_depth(),
|
177
|
+
"is_leaf": node.is_leaf(),
|
178
|
+
}
|
179
|
+
for node in nodes
|
180
|
+
]
|
181
|
+
return JsonResponse({"results": results})
|
182
|
+
|
183
|
+
|
184
|
+
# The End
|
treenode/widgets.py
CHANGED
@@ -2,64 +2,128 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode Widgets Module
|
4
4
|
|
5
|
-
This module defines custom form
|
6
|
-
within Django's admin interface. It
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
- `TreeWidget`: A custom Select2 widget that enhances usability for
|
11
|
-
hierarchical models.
|
12
|
-
- Automatically fetches hierarchical data via AJAX.
|
13
|
-
- Supports dynamic model binding for reusable implementations.
|
14
|
-
- Integrates with Django’s form system.
|
15
|
-
|
16
|
-
Version: 2.0.0
|
5
|
+
This module defines a custom form widget for handling hierarchical data
|
6
|
+
within Django's admin interface. It replaces the standard <select> dropdown
|
7
|
+
with a fully customizable tree selection UI.
|
8
|
+
|
9
|
+
Version: 2.1.0
|
17
10
|
Author: Timur Kady
|
18
11
|
Email: timurkady@yandex.com
|
19
12
|
"""
|
20
13
|
|
21
|
-
|
14
|
+
import json
|
22
15
|
from django import forms
|
16
|
+
from django.urls import reverse
|
17
|
+
from django.utils.safestring import mark_safe
|
18
|
+
from django.utils.translation import gettext_lazy as _
|
23
19
|
|
24
20
|
|
25
|
-
class TreeWidget(forms.
|
26
|
-
"""Custom
|
21
|
+
class TreeWidget(forms.Widget):
|
22
|
+
"""Custom widget for hierarchical tree selection."""
|
27
23
|
|
28
24
|
class Media:
|
29
|
-
"""
|
30
|
-
|
31
|
-
css = {
|
32
|
-
|
33
|
-
"https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css",
|
34
|
-
"treenode/css/tree_widget.css",
|
35
|
-
)
|
36
|
-
}
|
37
|
-
js = (
|
38
|
-
"https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js",
|
39
|
-
"treenode/js/tree_widget.js",
|
40
|
-
)
|
25
|
+
"""Meta class to define required CSS and JS files."""
|
26
|
+
|
27
|
+
css = {"all": ("treenode/css/tree_widget.css",)}
|
28
|
+
js = ("treenode/js/tree_widget.js",)
|
41
29
|
|
42
30
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
43
|
-
"""
|
31
|
+
"""Build attributes for the widget."""
|
44
32
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
45
|
-
attrs.setdefault("data-url", "
|
46
|
-
|
47
|
-
attrs["class"] = f"{
|
48
|
-
if "placeholder" in attrs:
|
49
|
-
del attrs["placeholder"]
|
33
|
+
attrs.setdefault("data-url", reverse("tree_autocomplete"))
|
34
|
+
attrs.setdefault("data-url-children", reverse("tree_children"))
|
35
|
+
attrs["class"] = f"{attrs.get('class', '')} tree-widget".strip()
|
50
36
|
|
51
|
-
# Принудительно передаём `model`
|
52
37
|
if "data-forward" not in attrs:
|
53
|
-
|
38
|
+
model = getattr(self, "model", None)
|
39
|
+
|
40
|
+
if not model and hasattr(self.choices, "queryset"):
|
54
41
|
model = self.choices.queryset.model
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
42
|
+
if model is None:
|
43
|
+
raise ValueError("TreeWidget: model not passed or not defined")
|
44
|
+
|
45
|
+
try:
|
46
|
+
forward_data = json.dumps({"model": model._meta.label})
|
47
|
+
attrs["data-forward"] = forward_data.replace('"', """)
|
48
|
+
except AttributeError as e:
|
49
|
+
raise ValueError("TreeWidget: invalid Django model") from e
|
50
|
+
|
51
|
+
if self.choices:
|
52
|
+
try:
|
53
|
+
current_value = self.value()
|
54
|
+
if current_value:
|
55
|
+
attrs["data-selected"] = str(current_value)
|
59
56
|
except Exception:
|
60
|
-
|
57
|
+
pass
|
61
58
|
|
62
59
|
return attrs
|
63
60
|
|
61
|
+
def optgroups(self, name, value, attrs=None):
|
62
|
+
"""
|
63
|
+
Override optgroups to return an empty structure.
|
64
|
+
|
65
|
+
The dropdown will be rendered dynamically via JS.
|
66
|
+
"""
|
67
|
+
return []
|
68
|
+
|
69
|
+
def render(self, name, value, attrs=None, renderer=None):
|
70
|
+
"""Render widget as a hidden input + tree container structure."""
|
71
|
+
|
72
|
+
attrs = self.build_attrs(attrs)
|
73
|
+
attrs["name"] = name
|
74
|
+
attrs["type"] = "hidden"
|
75
|
+
if value:
|
76
|
+
attrs["value"] = str(value)
|
77
|
+
|
78
|
+
# If value is set, try to get string representation of instance,
|
79
|
+
# otherwise print "Root"
|
80
|
+
if value not in [None, "", "None"]:
|
81
|
+
try:
|
82
|
+
from django.apps import apps
|
83
|
+
# Define the model: first self.model if it is defined,
|
84
|
+
# otherwise via choices
|
85
|
+
model = getattr(self, "model", None)
|
86
|
+
if model is None and hasattr(self, "choices") and getattr(self.choices, "queryset", None):
|
87
|
+
model = self.choices.queryset.model
|
88
|
+
if model is not None:
|
89
|
+
instance = model.objects.get(pk=value)
|
90
|
+
selected_value = str(instance)
|
91
|
+
else:
|
92
|
+
selected_value = str(value)
|
93
|
+
except Exception:
|
94
|
+
selected_value = str(value)
|
95
|
+
else:
|
96
|
+
selected_value = _("Root")
|
97
|
+
|
98
|
+
# Remove the 'tree-widget' class from the input so it doesn't interfere
|
99
|
+
# with container initialization
|
100
|
+
if "class" in attrs:
|
101
|
+
attrs["class"] = " ".join(
|
102
|
+
[cl for cl in attrs["class"].split() if cl != "tree-widget"])
|
103
|
+
|
104
|
+
html = """
|
105
|
+
<div class="tree-widget">
|
106
|
+
<div class="tree-widget-display">
|
107
|
+
<span class="selected-node">{selected_value}</span>
|
108
|
+
<span class="tree-dropdown-arrow">▼</span>
|
109
|
+
</div>
|
110
|
+
<input {attrs} />
|
111
|
+
<div class="tree-widget-dropdown">
|
112
|
+
<div class="tree-search-wrapper">
|
113
|
+
<span class="tree-search-icon">🔎︎</span>
|
114
|
+
<input type="text" class="tree-search" placeholder="{search_placeholder}" />
|
115
|
+
<button type="button" class="tree-search-clear">×</button>
|
116
|
+
</div>
|
117
|
+
<ul class="tree-list"></ul>
|
118
|
+
</div>
|
119
|
+
</div>
|
120
|
+
""".format(
|
121
|
+
attrs=" ".join(f'{key}="{val}"' for key, val in attrs.items()),
|
122
|
+
search_placeholder=_("Search node..."),
|
123
|
+
selected_value=selected_value
|
124
|
+
)
|
125
|
+
|
126
|
+
return mark_safe(html)
|
127
|
+
|
64
128
|
|
65
129
|
# The End
|