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.
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.0.8.dist-info}/METADATA +1 -1
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.0.8.dist-info}/RECORD +18 -18
- treenode/admin/admin.py +104 -126
- treenode/managers/queries.py +1 -1
- treenode/managers/tasks.py +1 -1
- treenode/static/css/treenode_admin.css +21 -0
- treenode/static/js/treenode_admin.js +130 -339
- treenode/templates/treenode/admin/treenode_changelist.html +61 -15
- treenode/templates/treenode/admin/treenode_rows.html +37 -40
- treenode/templatetags/treenode_admin.py +57 -14
- treenode/version.py +2 -2
- treenode/views/autoapi.py +91 -91
- treenode/views/autocomplete.py +52 -52
- treenode/views/children.py +41 -41
- treenode/views/common.py +23 -23
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.0.8.dist-info}/WHEEL +0 -0
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.0.8.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.0.8.dist-info}/top_level.txt +0 -0
@@ -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
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
{%
|
15
|
-
|
16
|
-
|
17
|
-
{% endif %}
|
47
|
+
{% if action_form and actions_on_top and cl.show_admin_actions %}
|
48
|
+
{% admin_actions %}
|
49
|
+
{% endif %}
|
18
50
|
|
19
|
-
|
51
|
+
{% block result_list %}
|
52
|
+
{% tree_result_list cl %}
|
53
|
+
{% endblock %}
|
20
54
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
</
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
3
|
+
Custom tags for changelist template.
|
4
4
|
|
5
|
-
Version: 3.
|
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"> </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":
|
75
|
+
"attrs": " ".join(f'{k}="{v}"' for k, v in row_attrs.items()),
|
35
76
|
"cells": cells,
|
36
|
-
"form": None,
|
77
|
+
"form": None,
|
78
|
+
"is_leaf": is_leaf,
|
79
|
+
"node_id": obj.pk,
|
37
80
|
})
|
38
81
|
|
39
82
|
return {
|
treenode/version.py
CHANGED
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
|
treenode/views/autocomplete.py
CHANGED
@@ -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})
|
treenode/views/children.py
CHANGED
@@ -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
|