django-fast-treenode 2.1.5__py3-none-any.whl → 3.0.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-3.0.0.dist-info/METADATA +203 -0
- django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
- {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +0 -5
- treenode/admin/admin.py +137 -208
- treenode/admin/changelist.py +21 -39
- treenode/admin/exporter.py +170 -0
- treenode/admin/importer.py +171 -0
- treenode/admin/mixin.py +291 -0
- treenode/apps.py +42 -20
- treenode/cache.py +192 -303
- treenode/forms.py +45 -65
- treenode/managers/__init__.py +4 -20
- treenode/managers/managers.py +216 -0
- treenode/managers/queries.py +233 -0
- treenode/managers/tasks.py +167 -0
- treenode/models/__init__.py +8 -5
- treenode/models/decorators.py +54 -0
- treenode/models/factory.py +44 -68
- treenode/models/mixins/__init__.py +2 -1
- treenode/models/mixins/ancestors.py +44 -20
- treenode/models/mixins/children.py +33 -26
- treenode/models/mixins/descendants.py +33 -22
- treenode/models/mixins/family.py +25 -15
- treenode/models/mixins/logical.py +23 -21
- treenode/models/mixins/node.py +162 -104
- treenode/models/mixins/properties.py +22 -16
- treenode/models/mixins/roots.py +59 -15
- treenode/models/mixins/siblings.py +46 -43
- treenode/models/mixins/tree.py +212 -153
- treenode/models/mixins/update.py +154 -0
- treenode/models/models.py +365 -0
- treenode/settings.py +28 -0
- treenode/static/{treenode/css → css}/tree_widget.css +1 -1
- treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
- treenode/static/css/treenode_tabs.css +51 -0
- treenode/static/js/lz-string.min.js +1 -0
- treenode/static/{treenode/js → js}/tree_widget.js +9 -23
- treenode/static/js/treenode_admin.js +531 -0
- treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
- treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
- treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/index.html +297 -0
- treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
- treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
- treenode/static/vendors/jquery-ui/package.json +82 -0
- treenode/templates/admin/treenode_changelist.html +25 -0
- treenode/templates/admin/treenode_import_export.html +85 -0
- treenode/templates/admin/treenode_rows.html +57 -0
- treenode/tests.py +3 -0
- treenode/urls.py +6 -27
- treenode/utils/__init__.py +0 -15
- treenode/utils/db/__init__.py +7 -0
- treenode/utils/db/compiler.py +114 -0
- treenode/utils/db/db_vendor.py +50 -0
- treenode/utils/db/service.py +84 -0
- treenode/utils/db/sqlcompat.py +60 -0
- treenode/utils/db/sqlquery.py +70 -0
- treenode/version.py +2 -2
- treenode/views/__init__.py +5 -0
- treenode/views/autoapi.py +91 -0
- treenode/views/autocomplete.py +52 -0
- treenode/views/children.py +41 -0
- treenode/views/common.py +23 -0
- treenode/views/crud.py +209 -0
- treenode/views/search.py +48 -0
- treenode/widgets.py +27 -44
- django_fast_treenode-2.1.5.dist-info/METADATA +0 -165
- django_fast_treenode-2.1.5.dist-info/RECORD +0 -63
- treenode/admin/mixins.py +0 -302
- treenode/managers/adjacency.py +0 -205
- treenode/managers/closure.py +0 -278
- treenode/models/adjacency.py +0 -342
- treenode/models/classproperty.py +0 -27
- treenode/models/closure.py +0 -122
- treenode/static/treenode/js/.gitkeep +0 -1
- treenode/static/treenode/js/treenode_admin.js +0 -131
- treenode/templates/admin/export_success.html +0 -26
- treenode/templates/admin/tree_node_changelist.html +0 -19
- treenode/templates/admin/tree_node_export.html +0 -27
- treenode/templates/admin/tree_node_import.html +0 -45
- treenode/templates/admin/tree_node_import_report.html +0 -32
- treenode/templates/widgets/tree_widget.css +0 -23
- treenode/utils/aid.py +0 -46
- treenode/utils/base16.py +0 -38
- treenode/utils/base36.py +0 -37
- treenode/utils/db.py +0 -116
- treenode/utils/exporter.py +0 -196
- treenode/utils/importer.py +0 -328
- treenode/utils/radix.py +0 -61
- treenode/views.py +0 -184
- {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
- /treenode/static/{treenode → css}/.gitkeep +0 -0
- /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -0,0 +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
|
treenode/views/common.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Created on Thu Apr 10 19:50:23 2025
|
4
|
+
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.apps import apps
|
11
|
+
from django.http import Http404
|
12
|
+
|
13
|
+
|
14
|
+
def get_model_from_request(request):
|
15
|
+
"""Get model from request."""
|
16
|
+
model_label = request.GET.get("model")
|
17
|
+
if not model_label:
|
18
|
+
raise Http404("Missing 'model' parameter.")
|
19
|
+
try:
|
20
|
+
app_label, model_name = model_label.lower().split(".")
|
21
|
+
return apps.get_model(app_label, model_name)
|
22
|
+
except Exception:
|
23
|
+
raise Http404(f"Invalid model format: {model_label}")
|
treenode/views/crud.py
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
API-First Support Module.
|
4
|
+
|
5
|
+
CRUD and Tree Operations for TreeNode models.
|
6
|
+
|
7
|
+
Version: 3.0.0
|
8
|
+
Author: Timur Kady
|
9
|
+
Email: timurkady@yandex.com
|
10
|
+
"""
|
11
|
+
|
12
|
+
import json
|
13
|
+
|
14
|
+
from django.forms.models import model_to_dict
|
15
|
+
from django.http import (
|
16
|
+
JsonResponse, HttpResponseBadRequest, HttpResponseNotFound,
|
17
|
+
)
|
18
|
+
|
19
|
+
from django.views import View
|
20
|
+
|
21
|
+
|
22
|
+
class TreeNodeBaseAPIView(View):
|
23
|
+
"""Simple API View for TreeNode-based models."""
|
24
|
+
|
25
|
+
model = None
|
26
|
+
|
27
|
+
def get_queryset(self):
|
28
|
+
"""Return base queryset."""
|
29
|
+
return self.model.objects.get_queryset()
|
30
|
+
|
31
|
+
def dispatch(self, request, *args, **kwargs):
|
32
|
+
"""Ddispatch request."""
|
33
|
+
self.action = kwargs.pop('action', None)
|
34
|
+
return super().dispatch(request, *args, **kwargs)
|
35
|
+
|
36
|
+
def get(self, request, pk=None):
|
37
|
+
"""
|
38
|
+
Handle GET requests depending on action.
|
39
|
+
|
40
|
+
Get a list of all nodes:
|
41
|
+
GET /treenode/api/<model>/?flat=true
|
42
|
+
|
43
|
+
Get one node:
|
44
|
+
GET /treenode/api/<model>/<id>/
|
45
|
+
|
46
|
+
Get the whole tree:
|
47
|
+
GET /treenode/api/<model>/tree/
|
48
|
+
|
49
|
+
Get the annotated tree:
|
50
|
+
GET /treenode/api/<model>/tree/?annotated=true
|
51
|
+
|
52
|
+
Only node own children:
|
53
|
+
GET /treenode/api/<model>/<id>/children/
|
54
|
+
|
55
|
+
All descendants:
|
56
|
+
GET /treenode/api/<model>/<id>/descendants/
|
57
|
+
|
58
|
+
Get a family:
|
59
|
+
GET /treenode/api/<model>/<id>/family/
|
60
|
+
"""
|
61
|
+
# Action mode
|
62
|
+
if self.action == "tree":
|
63
|
+
annotated = request.GET.get("annotated", "false").lower() == "true"
|
64
|
+
if annotated:
|
65
|
+
data = self.model.get_tree_annotated()
|
66
|
+
else:
|
67
|
+
data = self.model.get_tree()
|
68
|
+
return JsonResponse(data, safe=False)
|
69
|
+
|
70
|
+
if self.action == "children" and pk:
|
71
|
+
try:
|
72
|
+
node = self.get_queryset().get(pk=pk)
|
73
|
+
children = node.get_children()
|
74
|
+
data = [model_to_dict(obj) for obj in children]
|
75
|
+
return JsonResponse(data, safe=False)
|
76
|
+
except self.model.DoesNotExist:
|
77
|
+
return HttpResponseNotFound("Node not found.")
|
78
|
+
|
79
|
+
if self.action == "descendants" and pk:
|
80
|
+
try:
|
81
|
+
node = self.get_queryset().get(pk=pk)
|
82
|
+
descendants = node.get_descendants()
|
83
|
+
data = [model_to_dict(obj) for obj in descendants]
|
84
|
+
return JsonResponse(data, safe=False)
|
85
|
+
except self.model.DoesNotExist:
|
86
|
+
return HttpResponseNotFound("Node not found.")
|
87
|
+
|
88
|
+
if self.action == "family" and pk:
|
89
|
+
try:
|
90
|
+
node = self.get_queryset().get(pk=pk)
|
91
|
+
family = node.get_family()
|
92
|
+
data = [model_to_dict(obj) for obj in family]
|
93
|
+
return JsonResponse(data, safe=False)
|
94
|
+
except self.model.DoesNotExist:
|
95
|
+
return HttpResponseNotFound("Node not found.")
|
96
|
+
|
97
|
+
# Standard mode (if there is no special action)
|
98
|
+
if pk is None:
|
99
|
+
nodes = self.get_queryset().order_by("_path")
|
100
|
+
data = [model_to_dict(obj) for obj in nodes]
|
101
|
+
return JsonResponse(data, safe=False)
|
102
|
+
else:
|
103
|
+
try:
|
104
|
+
obj = self.get_queryset().get(pk=pk)
|
105
|
+
return JsonResponse(model_to_dict(obj))
|
106
|
+
except self.model.DoesNotExist:
|
107
|
+
return HttpResponseNotFound("Node not found.")
|
108
|
+
|
109
|
+
def post(self, request):
|
110
|
+
"""
|
111
|
+
Create a new node.
|
112
|
+
|
113
|
+
POST /treenode/api/<model>/
|
114
|
+
|
115
|
+
Body (JSON):
|
116
|
+
{
|
117
|
+
"name": "Node Name",
|
118
|
+
"parent_id": 123, # optional
|
119
|
+
"priority": 0
|
120
|
+
}
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
- Created node as JSON
|
124
|
+
"""
|
125
|
+
try:
|
126
|
+
body = json.loads(request.body)
|
127
|
+
obj = self.model(**body)
|
128
|
+
obj.full_clean()
|
129
|
+
obj.save()
|
130
|
+
return JsonResponse(model_to_dict(obj), status=201)
|
131
|
+
except Exception as e:
|
132
|
+
return HttpResponseBadRequest(str(e))
|
133
|
+
|
134
|
+
def put(self, request, pk):
|
135
|
+
"""
|
136
|
+
Replace a node.
|
137
|
+
|
138
|
+
PUT /treenode/api/<model>/<id>/
|
139
|
+
|
140
|
+
Body (JSON):
|
141
|
+
{
|
142
|
+
"name": "New Name",
|
143
|
+
"parent_id": 124,
|
144
|
+
"priority": 1
|
145
|
+
}
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
- Updated node as JSON
|
149
|
+
"""
|
150
|
+
try:
|
151
|
+
obj = self.get_queryset().get(pk=pk)
|
152
|
+
body = json.loads(request.body)
|
153
|
+
for field, value in body.items():
|
154
|
+
setattr(obj, field, value)
|
155
|
+
obj.full_clean()
|
156
|
+
obj.save()
|
157
|
+
return JsonResponse(model_to_dict(obj))
|
158
|
+
except self.model.DoesNotExist:
|
159
|
+
return HttpResponseNotFound("Node not found.")
|
160
|
+
except Exception as e:
|
161
|
+
return HttpResponseBadRequest(str(e))
|
162
|
+
|
163
|
+
def patch(self, request, pk):
|
164
|
+
"""
|
165
|
+
Update a node (partially).
|
166
|
+
|
167
|
+
PATCH /treenode/api/<model>/<id>/
|
168
|
+
|
169
|
+
Body (JSON):
|
170
|
+
{
|
171
|
+
"priority": 2
|
172
|
+
}
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
- Updated node as JSON
|
176
|
+
"""
|
177
|
+
return self.put(request, pk)
|
178
|
+
|
179
|
+
def delete(self, request, pk):
|
180
|
+
"""
|
181
|
+
Delete a node.
|
182
|
+
|
183
|
+
DELETE /treenode/api/<model>/<id>/
|
184
|
+
?cascade=true # default, delete node and descendants
|
185
|
+
?cascade=false # move children up before deleting
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
{
|
189
|
+
"status": "deleted",
|
190
|
+
"id": <deleted id>,
|
191
|
+
"cascade": true|false
|
192
|
+
}
|
193
|
+
"""
|
194
|
+
try:
|
195
|
+
cascade = not request.GET.get("cascade") == "false"
|
196
|
+
obj = self.get_queryset().get(pk=pk)
|
197
|
+
obj.delete(cascade=cascade)
|
198
|
+
return JsonResponse({
|
199
|
+
"status": "deleted",
|
200
|
+
"id": obj.pk,
|
201
|
+
"cascade": cascade
|
202
|
+
})
|
203
|
+
except self.model.DoesNotExist:
|
204
|
+
return HttpResponseNotFound("Node not found.")
|
205
|
+
except Exception as e:
|
206
|
+
return HttpResponseBadRequest(str(e))
|
207
|
+
|
208
|
+
|
209
|
+
# The End
|
treenode/views/search.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Created on Thu Apr 10 19:53:56 2025
|
4
|
+
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.http import JsonResponse
|
11
|
+
from django.views import View
|
12
|
+
from django.utils.decorators import method_decorator
|
13
|
+
from django.contrib.admin.views.decorators import staff_member_required
|
14
|
+
from django.apps import apps
|
15
|
+
from django.db.models import Q
|
16
|
+
|
17
|
+
|
18
|
+
@method_decorator(staff_member_required, name="dispatch")
|
19
|
+
class TreeSearchView(View):
|
20
|
+
"""Search view for TreeNode models used in admin interface."""
|
21
|
+
|
22
|
+
def get(self, request, *args, **kwargs):
|
23
|
+
app_label = request.GET.get("app")
|
24
|
+
model_name = request.GET.get("model")
|
25
|
+
query = request.GET.get("q", "").strip()
|
26
|
+
|
27
|
+
if not (app_label and model_name and query):
|
28
|
+
return JsonResponse({"results": []})
|
29
|
+
|
30
|
+
try:
|
31
|
+
model = apps.get_model(app_label, model_name)
|
32
|
+
except LookupError:
|
33
|
+
return JsonResponse({"results": []})
|
34
|
+
|
35
|
+
# Получаем queryset и ищем по __str__, через Q с contains
|
36
|
+
queryset = model.objects.all()
|
37
|
+
queryset = queryset.filter(
|
38
|
+
Q(name__icontains=query) | Q(pk__icontains=query))[:20]
|
39
|
+
|
40
|
+
results = [
|
41
|
+
{
|
42
|
+
"id": obj.pk,
|
43
|
+
"text": str(obj),
|
44
|
+
"is_leaf": obj.is_leaf(),
|
45
|
+
}
|
46
|
+
for obj in queryset
|
47
|
+
]
|
48
|
+
return JsonResponse({"results": results})
|
treenode/widgets.py
CHANGED
@@ -24,37 +24,22 @@ class TreeWidget(forms.Widget):
|
|
24
24
|
class Media:
|
25
25
|
"""Meta class to define required CSS and JS files."""
|
26
26
|
|
27
|
-
css = {"all": ("
|
28
|
-
js = ("
|
27
|
+
css = {"all": ("css/tree_widget.css",)}
|
28
|
+
js = ("js/tree_widget.js",)
|
29
29
|
|
30
30
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
31
31
|
"""Build attributes for the widget."""
|
32
32
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
33
|
-
attrs.setdefault("data-url", reverse("tree_autocomplete"))
|
34
|
-
attrs.setdefault("data-url-children", reverse("tree_children"))
|
33
|
+
attrs.setdefault("data-url", reverse("treenode:tree_autocomplete"))
|
34
|
+
attrs.setdefault("data-url-children", reverse("treenode:tree_children"))
|
35
35
|
attrs["class"] = f"{attrs.get('class', '')} tree-widget".strip()
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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)
|
56
|
-
except Exception:
|
57
|
-
pass
|
37
|
+
model = getattr(self, "model", None)
|
38
|
+
if not model and hasattr(self.choices, "queryset"):
|
39
|
+
model = self.choices.queryset.model
|
40
|
+
elif model is None:
|
41
|
+
raise ValueError("TreeWidget: model not passed or not defined")
|
42
|
+
attrs["data-model"] = model._meta.label
|
58
43
|
|
59
44
|
return attrs
|
60
45
|
|
@@ -66,32 +51,30 @@ class TreeWidget(forms.Widget):
|
|
66
51
|
"""
|
67
52
|
return []
|
68
53
|
|
54
|
+
def id_for_label(self, id_):
|
55
|
+
"""Return label for field."""
|
56
|
+
return f"{id_}_search"
|
57
|
+
|
58
|
+
def label_from_instance(self, obj):
|
59
|
+
"""Return instance label."""
|
60
|
+
return str(obj)
|
61
|
+
|
69
62
|
def render(self, name, value, attrs=None, renderer=None):
|
70
63
|
"""Render widget as a hidden input + tree container structure."""
|
71
|
-
|
72
64
|
attrs = self.build_attrs(attrs)
|
73
65
|
attrs["name"] = name
|
74
66
|
attrs["type"] = "hidden"
|
75
|
-
if value
|
76
|
-
attrs["value"] = str(value)
|
67
|
+
attrs["value"] = str(value) if value else ""
|
77
68
|
|
78
69
|
# If value is set, try to get string representation of instance,
|
79
70
|
# otherwise print "Root"
|
80
|
-
if value not in
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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)
|
71
|
+
if str(value).lower() not in ("", "none"):
|
72
|
+
model = self.model or self.choices.queryset.model
|
73
|
+
instance = model.objects.filter(pk=value).first()
|
74
|
+
if instance:
|
75
|
+
selected_value = str(instance)
|
76
|
+
else:
|
77
|
+
selected_value = _("Root")
|
95
78
|
else:
|
96
79
|
selected_value = _("Root")
|
97
80
|
|
@@ -111,7 +94,7 @@ class TreeWidget(forms.Widget):
|
|
111
94
|
<div class="tree-widget-dropdown">
|
112
95
|
<div class="tree-search-wrapper">
|
113
96
|
<span class="tree-search-icon">🔎︎</span>
|
114
|
-
<input type="text" class="tree-search" placeholder="{search_placeholder}" />
|
97
|
+
<input id="id_parent_search" type="text" class="tree-search" placeholder="{search_placeholder}" />
|
115
98
|
<button type="button" class="tree-search-clear">×</button>
|
116
99
|
</div>
|
117
100
|
<ul class="tree-list"></ul>
|
@@ -1,165 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: django-fast-treenode
|
3
|
-
Version: 2.1.5
|
4
|
-
Summary: Application for supporting tree (hierarchical) data structure in Django projects
|
5
|
-
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
|
-
Author: Timur Kady
|
7
|
-
Author-email: Timur Kady <timurkady@yandex.com>
|
8
|
-
License: MIT License
|
9
|
-
|
10
|
-
Copyright (c) 2020-2025 Timur Kady
|
11
|
-
|
12
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
-
of this software and associated documentation files (the "Software"), to deal
|
14
|
-
in the Software without restriction, including without limitation the rights
|
15
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
16
|
-
copies of the Software, and to permit persons to whom the Software is
|
17
|
-
furnished to do so, subject to the following conditions:
|
18
|
-
|
19
|
-
The above copyright notice and this permission notice shall be included in all
|
20
|
-
copies or substantial portions of the Software.
|
21
|
-
|
22
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
27
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
|
-
SOFTWARE.
|
29
|
-
|
30
|
-
Project-URL: Homepage, https://github.com/TimurKady/django-fast-treenode
|
31
|
-
Project-URL: Documentation, https://django-fast-treenode.readthedocs.io/
|
32
|
-
Project-URL: Source, https://github.com/TimurKady/django-fast-treenode
|
33
|
-
Project-URL: Issues, https://github.com/TimurKady/django-fast-treenode/issues
|
34
|
-
Classifier: Development Status :: 5 - Production/Stable
|
35
|
-
Classifier: Intended Audience :: Developers
|
36
|
-
Classifier: Programming Language :: Python :: 3
|
37
|
-
Classifier: Programming Language :: Python :: 3.9
|
38
|
-
Classifier: Programming Language :: Python :: 3.10
|
39
|
-
Classifier: Programming Language :: Python :: 3.11
|
40
|
-
Classifier: Programming Language :: Python :: 3.12
|
41
|
-
Classifier: Programming Language :: Python :: 3.13
|
42
|
-
Classifier: Programming Language :: Python :: 3.14
|
43
|
-
Classifier: Framework :: Django
|
44
|
-
Classifier: Framework :: Django :: 4.0
|
45
|
-
Classifier: Framework :: Django :: 4.1
|
46
|
-
Classifier: Framework :: Django :: 4.2
|
47
|
-
Classifier: Framework :: Django :: 5.0
|
48
|
-
Classifier: Framework :: Django :: 5.1
|
49
|
-
Classifier: Framework :: Django :: 5.2
|
50
|
-
Classifier: License :: OSI Approved :: MIT License
|
51
|
-
Classifier: Operating System :: OS Independent
|
52
|
-
Requires-Python: >=3.9
|
53
|
-
Description-Content-Type: text/markdown
|
54
|
-
License-File: LICENSE
|
55
|
-
Requires-Dist: Django>=4.0
|
56
|
-
Requires-Dist: django-widget-tweaks>=1.5
|
57
|
-
Requires-Dist: msgpack>=1.1
|
58
|
-
Provides-Extra: import-export
|
59
|
-
Requires-Dist: openpyxl; extra == "import-export"
|
60
|
-
Requires-Dist: pyyaml; extra == "import-export"
|
61
|
-
Requires-Dist: xlsxwriter; extra == "import-export"
|
62
|
-
Dynamic: license-file
|
63
|
-
|
64
|
-
# Django-fast-treenode
|
65
|
-
**Hybrid Tree Storage**
|
66
|
-
|
67
|
-
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
68
|
-
[](https://django-fast-treenode.readthedocs.io/)
|
69
|
-
[](https://pypi.org/project/django-fast-treenode/)
|
70
|
-
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
71
|
-
[](https://github.com/sponsors/TimurKady)
|
72
|
-
|
73
|
-
**Django Fast TreeNode** is a high-performance Django application for working with tree structures.
|
74
|
-
|
75
|
-
## Features
|
76
|
-
- **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
|
77
|
-
- **Custom caching system**: A built-in caching mechanism, specifically designed for this package, significantly boosts execution speed.
|
78
|
-
- **Efficient queries**: Retrieve ancestors, descendants, breadcrumbs, and tree depth with only one SQL queriy.
|
79
|
-
- **Bulk operations**: Supports fast insertion, movement, and deletion of nodes.
|
80
|
-
- **Flexibility**: Fully integrates with Django ORM and adapts to various business logic needs.
|
81
|
-
- **Admin panel integration**: Full compatibility with Django's admin panel, allowing intuitive management of tree structures.
|
82
|
-
- **Import & Export functionality**: Built-in support for importing and exporting tree structures in multiple formats (CSV, JSON, XLSX, YAML, TSV), including integration with the Django admin panel.
|
83
|
-
|
84
|
-
It seems that django-fast-treenode is currently the most balanced and performant solution for most tasks, especially those related to dynamic hierarchical data structures. Check out the results of (comparison tests)[#] with other Django packages.
|
85
|
-
|
86
|
-
## Use Cases
|
87
|
-
Django Fast TreeNode is suitable for a wide range of applications, from simple directories to complex systems with deep hierarchical structures:
|
88
|
-
- **Categories and taxonomies**: Manage product categories, tags, and classification systems.
|
89
|
-
- **Menus and navigation**: Create tree-like menus and nested navigation structures.
|
90
|
-
- **Forums and comments**: Store threaded discussions and nested comment chains.
|
91
|
-
- **Geographical data**: Represent administrative divisions, regions, and areas of influence.
|
92
|
-
- **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
|
93
|
-
|
94
|
-
|
95
|
-
## Quick start
|
96
|
-
1. Run `pip install django-fast-treenode`.
|
97
|
-
2. Add `treenode` to `settings.INSTALLED_APPS`.
|
98
|
-
|
99
|
-
```python
|
100
|
-
INSTALLED_APPS = [
|
101
|
-
...
|
102
|
-
'treenode',
|
103
|
-
]
|
104
|
-
```
|
105
|
-
|
106
|
-
3. Define your model inherit from `treenode.models.TreeNodeModel`.
|
107
|
-
|
108
|
-
```python
|
109
|
-
from treenode.models import TreeNodeModel
|
110
|
-
|
111
|
-
class Category(TreeNodeModel):
|
112
|
-
name = models.CharField(max_length=255)
|
113
|
-
treenode_display_field = "name"
|
114
|
-
```
|
115
|
-
|
116
|
-
4. Make your model-admin inherit from `treenode.admin.TreeNodeModelAdmin`.
|
117
|
-
|
118
|
-
```python
|
119
|
-
from treenode.admin import TreeNodeModelAdmin
|
120
|
-
from .models import Category
|
121
|
-
|
122
|
-
@admin.register(Category)
|
123
|
-
class CategoryAdmin(TreeNodeModelAdmin):
|
124
|
-
list_display = ("name",)
|
125
|
-
search_fields = ("name",)
|
126
|
-
```
|
127
|
-
5. Run migrations.
|
128
|
-
|
129
|
-
```bash
|
130
|
-
python manage.py makemigrations
|
131
|
-
python manage.py migrate
|
132
|
-
```
|
133
|
-
|
134
|
-
6. Run server and use!
|
135
|
-
|
136
|
-
```bash
|
137
|
-
>>> root = Category.objects.create(name="Root")
|
138
|
-
>>> child = Category.objects.create(name="Child")
|
139
|
-
>>> child.set_parent(root)
|
140
|
-
>>> root_descendants_list = root.get_descendants()
|
141
|
-
>>> root_children_queryset = root.get_children_queryset()
|
142
|
-
>>> ancestors_pks = child.get_ancestors_pks()
|
143
|
-
```
|
144
|
-
|
145
|
-
## Documentation
|
146
|
-
Full documentation is available at **[ReadTheDocs](https://django-fast-treenode.readthedocs.io/)**.
|
147
|
-
|
148
|
-
Quick access links:
|
149
|
-
* [Installation, configuration and fine tuning](https://django-fast-treenode.readthedocs.io/installation/)
|
150
|
-
* [Model Inheritance and Extensions](https://django-fast-treenode.readthedocs.io/models/)
|
151
|
-
* [Working with Admin Classes](https://django-fast-treenode.readthedocs.io/admin/)
|
152
|
-
* [API Reference](https://django-fast-treenode.readthedocs.io/api/)
|
153
|
-
* [Import & Export](https://django-fast-treenode.readthedocs.io/import_export/)
|
154
|
-
* [Caching and working with cache](https://django-fast-treenode.readthedocs.io/cache/)
|
155
|
-
* [Migration and upgrade guide](https://django-fast-treenode.readthedocs.io/migration/)
|
156
|
-
|
157
|
-
Your wishes, objections, comments are welcome.
|
158
|
-
|
159
|
-
## License
|
160
|
-
Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
161
|
-
|
162
|
-
## Credits
|
163
|
-
Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
|
164
|
-
|
165
|
-
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
|
@@ -1,63 +0,0 @@
|
|
1
|
-
django_fast_treenode-2.1.5.dist-info/licenses/LICENSE,sha256=T0evsb7y-63fg18ovdNSx3wwWWRwyluQvN9J4zFSvfE,1093
|
2
|
-
treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
treenode/apps.py,sha256=a7UasXiZZudPccjmHEudP79TkhR_53Mvnb-dBXLHRRQ,862
|
4
|
-
treenode/cache.py,sha256=GoN2J-ypEQWIK05WSw9LYo7boKHGPXNFxqHorFPUqX8,12481
|
5
|
-
treenode/forms.py,sha256=Mjrpuyd1CPsitcElDVagE3k-p2kU4xIlRuy1f5Zgt3c,3800
|
6
|
-
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
7
|
-
treenode/urls.py,sha256=CsgX0hRyDVrMS8YnRlr_CxmDlgGIhDpqZ9ldoMYZCac,866
|
8
|
-
treenode/version.py,sha256=mv001KtDXkO8dnqphEI_VEhvLuHmmmhI515DitY0q2U,222
|
9
|
-
treenode/views.py,sha256=rEZEgdbEA3AJDHrvtrAm-t60QTJcJ4JEhNsNMR1Y_I4,5549
|
10
|
-
treenode/widgets.py,sha256=Mi0F-AK_UcmU6C50ENK9vv6xGQNuDtrtzXSnXSOXhLM,4760
|
11
|
-
treenode/admin/__init__.py,sha256=K5GgagrfLwzF8GvOYfwXpJYLCexM8DbEoK1bhsqIBvc,119
|
12
|
-
treenode/admin/admin.py,sha256=iVi8s8mPVVDlbbJFqCcuXzDbE29KPj6XntFDDRECkmY,10580
|
13
|
-
treenode/admin/changelist.py,sha256=YZm3zNniX75CgLjnbHpVr0OIP91halDEBHmrcS8m5Og,2128
|
14
|
-
treenode/admin/mixins.py,sha256=-dVZwEjKsfRzMkBe87dkI0SZ9MH45YE_o39SIhSJWy4,11194
|
15
|
-
treenode/managers/__init__.py,sha256=EG_tj9P1Hama3kaqMfHck4lfzUWoPaJJVOXe3qaKMUo,585
|
16
|
-
treenode/managers/adjacency.py,sha256=OOjHCSTo0aAcSxOOwz7OsQTGdTRkM1mAxSN7jlzRpho,7896
|
17
|
-
treenode/managers/closure.py,sha256=PcScdJJUnLcKe8Y1wqROYPsRtAnBMUO4xn5sILk9AIM,10638
|
18
|
-
treenode/models/__init__.py,sha256=pBiMlEpC_Thh7asraNzA7W_7BKu2oAHtcn-K6_sdJe8,112
|
19
|
-
treenode/models/adjacency.py,sha256=QWGOidd4tH3afqVedPNQqeh-W-zUTNs1m-iAhCAXub4,12396
|
20
|
-
treenode/models/classproperty.py,sha256=J4W6snsfsEUSHKHkIlM9yOJYQ_FSrp3P3oEYMKJengg,571
|
21
|
-
treenode/models/closure.py,sha256=eZtLbnCOR1xYWhgbo1Pml_K0Pd0MM2DjiZl3SWMVe2A,3712
|
22
|
-
treenode/models/factory.py,sha256=10FEGGC5PGWaR58qErs0oOrCS0KeI8x9H-SknZAAWqw,2291
|
23
|
-
treenode/models/mixins/__init__.py,sha256=gTdMZFh1slNHMvxrnu-hGl46xqnWd4W7TOEFWTVJq40,757
|
24
|
-
treenode/models/mixins/ancestors.py,sha256=QZywMcIVZK82j13QsgevVN2ZhRLa86DfRIt2BsiM2to,1526
|
25
|
-
treenode/models/mixins/children.py,sha256=xgenQFyZBG7_S33QQlznSmNhXEdeo9DeLyi7dKmvFhw,2637
|
26
|
-
treenode/models/mixins/descendants.py,sha256=PYYfd7oqlv3Gnfahm0u9ACHjpWSDNM6Z8oJaJXPVQ8w,1910
|
27
|
-
treenode/models/mixins/family.py,sha256=h2IRRADkQxve97QqBHKv0evVz4cFQtcNR8CbPi9Ri_w,1645
|
28
|
-
treenode/models/mixins/logical.py,sha256=jlhBSq3AfCYNyNjqyKM9siyioS3SYcGD-aG2b4MV2RM,2169
|
29
|
-
treenode/models/mixins/node.py,sha256=VpLiFI1olvj5Gp2yV4n-aG4z4mZ7vOS6ytloWwO5s6w,7149
|
30
|
-
treenode/models/mixins/properties.py,sha256=pfv80KLXcPeGx00IFCBcst1_cf0AmzhjshFjq1XQWMY,3876
|
31
|
-
treenode/models/mixins/roots.py,sha256=MoFQq1fph70awc26UMUbfeTpt0ToUOvMz1c7LlDyIP8,2956
|
32
|
-
treenode/models/mixins/siblings.py,sha256=JTQjaxnDH9t-AVMCQFiuc0nHLdIsE4v5vJ5z6LcUZLY,3236
|
33
|
-
treenode/models/mixins/tree.py,sha256=CsO0ynwcwkrWgQbTzvF4yws-y7n1GGM2zImJH0hgV00,13042
|
34
|
-
treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
|
-
treenode/static/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
36
|
-
treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
37
|
-
treenode/static/treenode/css/tree_widget.css,sha256=SE74hZaOECHe1VKe-N6b-MxcZ6tQrA9d4ctfNHrVvvA,4864
|
38
|
-
treenode/static/treenode/css/treenode_admin.css,sha256=7Ye_bCgIgG-QUcih1jXIda1XxhAkTFLU-0CHcKNCZtw,2238
|
39
|
-
treenode/static/treenode/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
40
|
-
treenode/static/treenode/js/tree_widget.js,sha256=SGxm2Awu1Ysk53h1r8JIS5wo9XGQFUD0cz9WqsMQXMs,10331
|
41
|
-
treenode/static/treenode/js/treenode_admin.js,sha256=q_OvlDQPvmv9SfV2u69PztVY2Yrdl4qdlnoAzbbkovA,4747
|
42
|
-
treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
43
|
-
treenode/templates/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
44
|
-
treenode/templates/admin/export_success.html,sha256=xN2D-BCH249CJB10fo_vHYUyFenQ9mFKqq7UTWcrXS4,747
|
45
|
-
treenode/templates/admin/tree_node_changelist.html,sha256=fGRVvWx2EnpiFYeJckyPKV-BCv9I13_ViiNN0LIUZKM,380
|
46
|
-
treenode/templates/admin/tree_node_export.html,sha256=vJxEoGI-US6VdFddxAFgL5r3MgGt6mgA43vltCsbA2k,1043
|
47
|
-
treenode/templates/admin/tree_node_import.html,sha256=unksxTAO2bJbxRkZfrCltHn61MgfqGt2sxIsUOW5dVk,1513
|
48
|
-
treenode/templates/admin/tree_node_import_report.html,sha256=azHJ8JFrSRu60lF1Uh22zs9JXQxZdvOjYdwCtlbaE3I,1133
|
49
|
-
treenode/templates/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
|
50
|
-
treenode/templates/widgets/tree_widget.css,sha256=2bEaxu1x7QJZ7erbs2SLMaxeaiMkjQXadfcDEW8wfok,551
|
51
|
-
treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
52
|
-
treenode/utils/__init__.py,sha256=B4bv96ivtHELPv0_DllJa5z-k1QMo7z-MKuvj-3NdtI,356
|
53
|
-
treenode/utils/aid.py,sha256=o8Jgc1vDRtQpx4XYdv0qR5Lqvens55Jfbdca1nr-EOA,1013
|
54
|
-
treenode/utils/base16.py,sha256=U1PMit2aZOpYusG_u1c7eVpXO-cFrFPyVyk9zdHrehg,817
|
55
|
-
treenode/utils/base36.py,sha256=yICmyPE-yyPNO9T2oALOt-b6uYf37ahFfx0R4tXn3X0,847
|
56
|
-
treenode/utils/db.py,sha256=36q4OckKmEd6uHTbMTxdKpV9nOIZ55DAantRWR9bxWg,4297
|
57
|
-
treenode/utils/exporter.py,sha256=LGC5VfJj7wMFp7BkaWjmfrImgCVRpJ8gjkDpn4IDTEs,7258
|
58
|
-
treenode/utils/importer.py,sha256=Hvirbd6NyZ2MHa56_jOrUF3kYFeby1DbSLR3mhHy-9s,12891
|
59
|
-
treenode/utils/radix.py,sha256=zHpOuDxsebiv9Gza6snNhAtBKiex6CDrAVRtB6esaWo,1642
|
60
|
-
django_fast_treenode-2.1.5.dist-info/METADATA,sha256=KpOVLmk1TDKx5_iXE1geP9_GlfmVIvJK-IhuSh7Lu8w,8103
|
61
|
-
django_fast_treenode-2.1.5.dist-info/WHEEL,sha256=DK49LOLCYiurdXXOXwGJm6U4DkHkg4lcxjhqwRa0CP4,91
|
62
|
-
django_fast_treenode-2.1.5.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
63
|
-
django_fast_treenode-2.1.5.dist-info/RECORD,,
|