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.
Files changed (70) hide show
  1. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
  3. django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
  4. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/docs/.gitignore +0 -0
  12. treenode/docs/about.md +36 -0
  13. treenode/docs/admin.md +104 -0
  14. treenode/docs/api.md +739 -0
  15. treenode/docs/cache.md +187 -0
  16. treenode/docs/import_export.md +35 -0
  17. treenode/docs/index.md +30 -0
  18. treenode/docs/installation.md +74 -0
  19. treenode/docs/migration.md +145 -0
  20. treenode/docs/models.md +128 -0
  21. treenode/docs/roadmap.md +45 -0
  22. treenode/forms.py +33 -22
  23. treenode/managers/__init__.py +21 -0
  24. treenode/managers/adjacency.py +203 -0
  25. treenode/managers/closure.py +278 -0
  26. treenode/models/__init__.py +2 -1
  27. treenode/models/adjacency.py +343 -0
  28. treenode/models/classproperty.py +3 -0
  29. treenode/models/closure.py +39 -65
  30. treenode/models/factory.py +12 -2
  31. treenode/models/mixins/__init__.py +23 -0
  32. treenode/models/mixins/ancestors.py +65 -0
  33. treenode/models/mixins/children.py +81 -0
  34. treenode/models/mixins/descendants.py +66 -0
  35. treenode/models/mixins/family.py +63 -0
  36. treenode/models/mixins/logical.py +68 -0
  37. treenode/models/mixins/node.py +210 -0
  38. treenode/models/mixins/properties.py +156 -0
  39. treenode/models/mixins/roots.py +96 -0
  40. treenode/models/mixins/siblings.py +99 -0
  41. treenode/models/mixins/tree.py +344 -0
  42. treenode/signals.py +26 -0
  43. treenode/static/treenode/css/tree_widget.css +201 -31
  44. treenode/static/treenode/css/treenode_admin.css +48 -41
  45. treenode/static/treenode/js/tree_widget.js +269 -131
  46. treenode/static/treenode/js/treenode_admin.js +131 -171
  47. treenode/templates/admin/tree_node_changelist.html +6 -0
  48. treenode/templates/admin/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {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.0.0
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
- from django.apps import apps
26
- from django.db.models import Case, When, Value, IntegerField
27
- from django.core.exceptions import ObjectDoesNotExist
26
+
27
+ import logging
28
+
29
+ logger = logging.getLogger(__name__)
28
30
 
29
31
 
30
32
  class TreeNodeAutocompleteView(View):
31
- """Returns JSON data for Select2 with tree structure."""
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
- """Get method."""
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
- model_label = request.GET.get("model") # Получаем модель
47
+ if q:
48
+ return self.search(request)
37
49
 
38
- if not model_label:
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": "Missing model parameter"},
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
- queryset = model.objects.filter(name__icontains=q)
53
- node_list = sorted(queryset, key=lambda x: x.tn_order)
54
- pk_list = [node.pk for node in node_list]
55
- nodes = queryset.filter(pk__in=pk_list).order_by(
56
- Case(*[When(pk=pk, then=Value(index))
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.get_level(),
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 GetChildrenCountView(View):
75
- """Return the number of children for a given parent node."""
141
+ class ChildrenView(View):
142
+ """Return JSON data for Select2 with node children."""
76
143
 
77
144
  def get(self, request):
78
- """Get method."""
79
- parent_id = request.GET.get("parent_id")
80
- model_label = request.GET.get("model") # Получаем модель
81
-
82
- if not model_label or not parent_id:
83
- return JsonResponse({"error": "Missing parameters"}, status=400)
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
- try:
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": "Parent node not found"},
100
- status=404
163
+ {"error": f"Invalid reference_id: {reference_id}"},
164
+ status=400
101
165
  )
102
166
 
103
- return JsonResponse({"children_count": children_count})
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 widgets for handling hierarchical data
6
- within Django's admin interface. It includes a Select2-based widget
7
- for tree-structured data selection.
8
-
9
- Features:
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.Select):
26
- """Custom Select2 widget for hierarchical data."""
21
+ class TreeWidget(forms.Widget):
22
+ """Custom widget for hierarchical tree selection."""
27
23
 
28
24
  class Media:
29
- """Mrta class."""
30
-
31
- css = {
32
- "all": (
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
- """Add attributes for Select2 integration."""
31
+ """Build attributes for the widget."""
44
32
  attrs = super().build_attrs(base_attrs, extra_attrs)
45
- attrs.setdefault("data-url", "/treenode/tree-autocomplete/")
46
- existing_class = attrs.get("class", "")
47
- attrs["class"] = f"{existing_class} tree-widget".strip()
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
- try:
38
+ model = getattr(self, "model", None)
39
+
40
+ if not model and hasattr(self.choices, "queryset"):
54
41
  model = self.choices.queryset.model
55
- label = model._meta.app_label
56
- model_name = model._meta.model_name
57
- model_label = f"{label}.{model_name}"
58
- attrs["data-forward"] = f'{{"model": "{model_label}"}}'
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('"', "&quot;")
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
- attrs["data-forward"] = '{"model": ""}'
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">&#x1F50E;&#xFE0E;</span>
114
+ <input type="text" class="tree-search" placeholder="{search_placeholder}" />
115
+ <button type="button" class="tree-search-clear">&times;</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