grid-view-spec 2.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.
- grid_view_spec/__init__.py +23 -0
- grid_view_spec/backends/django/__init__.py +43 -0
- grid_view_spec/backends/django/ag_grid/__init__.py +26 -0
- grid_view_spec/backends/django/ag_grid/export.py +26 -0
- grid_view_spec/backends/django/ag_grid/server.py +235 -0
- grid_view_spec/backends/django/apps.py +18 -0
- grid_view_spec/backends/django/assets.py +152 -0
- grid_view_spec/backends/django/conf.py +117 -0
- grid_view_spec/backends/django/export.py +89 -0
- grid_view_spec/backends/django/facets.py +68 -0
- grid_view_spec/backends/django/grid_preferences.py +16 -0
- grid_view_spec/backends/django/host.py +88 -0
- grid_view_spec/backends/django/hrefs.py +30 -0
- grid_view_spec/backends/django/i18n.py +24 -0
- grid_view_spec/backends/django/lazy.py +60 -0
- grid_view_spec/backends/django/migrations/0001_initial.py +33 -0
- grid_view_spec/backends/django/migrations/__init__.py +0 -0
- grid_view_spec/backends/django/models.py +26 -0
- grid_view_spec/backends/django/pdf_response.py +16 -0
- grid_view_spec/backends/django/prefs.py +53 -0
- grid_view_spec/backends/django/queryset_search.py +112 -0
- grid_view_spec/backends/django/search.py +47 -0
- grid_view_spec/backends/django/templatetags.py +119 -0
- grid_view_spec/backends/django/throttle.py +111 -0
- grid_view_spec/backends/django/urls.py +35 -0
- grid_view_spec/backends/django/views.py +242 -0
- grid_view_spec/backends/django/wire_templates.py +31 -0
- grid_view_spec/backends/fastapi/router.py +38 -0
- grid_view_spec/backends/fragment/html.py +107 -0
- grid_view_spec/backends/jinja2/renderer.py +21 -0
- grid_view_spec/backends/json/__init__.py +5 -0
- grid_view_spec/backends/json/wire.py +59 -0
- grid_view_spec/backends/starlette/host.py +23 -0
- grid_view_spec/backends/starlette/routes.py +31 -0
- grid_view_spec/backends/starlette/static.py +38 -0
- grid_view_spec/export/__init__.py +49 -0
- grid_view_spec/export/charts_png.py +51 -0
- grid_view_spec/export/columns.py +90 -0
- grid_view_spec/export/context.py +124 -0
- grid_view_spec/export/html.py +72 -0
- grid_view_spec/export/meta.py +120 -0
- grid_view_spec/export/payload.py +79 -0
- grid_view_spec/export/pdf/backends.py +44 -0
- grid_view_spec/export/pipeline.py +131 -0
- grid_view_spec/export/print.py +161 -0
- grid_view_spec/export/registry.py +100 -0
- grid_view_spec/export/rows.py +103 -0
- grid_view_spec/export/static_charts.py +216 -0
- grid_view_spec/export/xlsx.py +286 -0
- grid_view_spec/hosts/__init__.py +20 -0
- grid_view_spec/hosts/base.py +17 -0
- grid_view_spec/hosts/memory.py +77 -0
- grid_view_spec/i18n.py +118 -0
- grid_view_spec/locale/en/LC_MESSAGES/django.mo +0 -0
- grid_view_spec/locale/en/LC_MESSAGES/django.po +324 -0
- grid_view_spec/locale/uk/LC_MESSAGES/django.mo +0 -0
- grid_view_spec/locale/uk/LC_MESSAGES/django.po +330 -0
- grid_view_spec/mcp/__init__.py +23 -0
- grid_view_spec/mcp/__main__.py +10 -0
- grid_view_spec/mcp/a2ui.py +337 -0
- grid_view_spec/mcp/catalog.py +388 -0
- grid_view_spec/mcp/envelope.py +64 -0
- grid_view_spec/mcp/examples.py +131 -0
- grid_view_spec/mcp/fixtures.py +1188 -0
- grid_view_spec/mcp/normalize.py +62 -0
- grid_view_spec/mcp/schema.py +138 -0
- grid_view_spec/mcp/server.py +158 -0
- grid_view_spec/mcp/validate.py +48 -0
- grid_view_spec/py.typed +1 -0
- grid_view_spec/render/__init__.py +16 -0
- grid_view_spec/render/a2ui.py +277 -0
- grid_view_spec/render/action_urls.py +218 -0
- grid_view_spec/render/ag_grid.py +92 -0
- grid_view_spec/render/bind.py +101 -0
- grid_view_spec/render/block_registry.py +103 -0
- grid_view_spec/render/chart_runtime.py +51 -0
- grid_view_spec/render/chart_spec.py +97 -0
- grid_view_spec/render/charts_bind.py +265 -0
- grid_view_spec/render/column_settings.py +137 -0
- grid_view_spec/render/context.py +43 -0
- grid_view_spec/render/jinja.py +31 -0
- grid_view_spec/render/lazy.py +25 -0
- grid_view_spec/render/refs.py +25 -0
- grid_view_spec/render/request_state.py +109 -0
- grid_view_spec/render/row_template.py +53 -0
- grid_view_spec/render/spec_renderer.py +335 -0
- grid_view_spec/render/tab_panes.py +52 -0
- grid_view_spec/render/table_chart.py +36 -0
- grid_view_spec/render/table_edit.py +56 -0
- grid_view_spec/schema/grid-view-spec.v2.json +871 -0
- grid_view_spec/search/__init__.py +7 -0
- grid_view_spec/search/column.py +35 -0
- grid_view_spec/search/column_meta.py +35 -0
- grid_view_spec/search/column_scope.py +82 -0
- grid_view_spec/search/contract.py +318 -0
- grid_view_spec/search/engine.py +223 -0
- grid_view_spec/search/facet_registry.py +91 -0
- grid_view_spec/search/facets.py +166 -0
- grid_view_spec/search/params.py +8 -0
- grid_view_spec/search/query_commit.py +28 -0
- grid_view_spec/search/smart.py +319 -0
- grid_view_spec/search/syntax_tips.py +83 -0
- grid_view_spec/search/term_match.py +197 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-cdn.js +16 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-cdn.min.js +1 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-theme.css +250 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-theme.min.css +1 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid.js +1965 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid.min.js +1 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-charts.js +577 -0
- grid_view_spec/static/grid_view_spec/gridviewspec-charts.min.js +3 -0
- grid_view_spec/static/grid_view_spec/gridviewspec.css +3662 -0
- grid_view_spec/static/grid_view_spec/gridviewspec.js +7000 -0
- grid_view_spec/static/grid_view_spec/gridviewspec.min.css +1 -0
- grid_view_spec/static/grid_view_spec/gridviewspec.min.js +3 -0
- grid_view_spec/static/grid_view_spec/vendor/ag-grid-community.min.js +1 -0
- grid_view_spec/static/grid_view_spec/vendor/ag-grid.css +8644 -0
- grid_view_spec/static/grid_view_spec/vendor/ag-theme-quartz.css +865 -0
- grid_view_spec/templates/export/base.html +67 -0
- grid_view_spec/templates/export/blocks/charts.html +7 -0
- grid_view_spec/templates/export/blocks/table.html +60 -0
- grid_view_spec/templates/export/spec_report.html +12 -0
- grid_view_spec/templates/grid_view/assets/ag_grid_js.html +9 -0
- grid_view_spec/templates/grid_view/assets/css.html +15 -0
- grid_view_spec/templates/grid_view/assets/js.html +54 -0
- grid_view_spec/templates/grid_view/spec/_action_button.html +12 -0
- grid_view_spec/templates/grid_view/spec/_action_link.html +12 -0
- grid_view_spec/templates/grid_view/spec/_column_settings_panel.html +25 -0
- grid_view_spec/templates/grid_view/spec/_filter_bar.html +57 -0
- grid_view_spec/templates/grid_view/spec/_icon.html +10 -0
- grid_view_spec/templates/grid_view/spec/_semantic.html +17 -0
- grid_view_spec/templates/grid_view/spec/actions.html +36 -0
- grid_view_spec/templates/grid_view/spec/block.html +30 -0
- grid_view_spec/templates/grid_view/spec/card_groups.html +17 -0
- grid_view_spec/templates/grid_view/spec/cards.html +38 -0
- grid_view_spec/templates/grid_view/spec/charts.html +15 -0
- grid_view_spec/templates/grid_view/spec/content.html +15 -0
- grid_view_spec/templates/grid_view/spec/filters.html +4 -0
- grid_view_spec/templates/grid_view/spec/form.html +5 -0
- grid_view_spec/templates/grid_view/spec/gallery.html +5 -0
- grid_view_spec/templates/grid_view/spec/header.html +60 -0
- grid_view_spec/templates/grid_view/spec/image.html +3 -0
- grid_view_spec/templates/grid_view/spec/kpi.html +14 -0
- grid_view_spec/templates/grid_view/spec/lazy_placeholder.html +9 -0
- grid_view_spec/templates/grid_view/spec/nav.html +5 -0
- grid_view_spec/templates/grid_view/spec/overlay.html +3 -0
- grid_view_spec/templates/grid_view/spec/spec.html +46 -0
- grid_view_spec/templates/grid_view/spec/table.html +336 -0
- grid_view_spec/templates/grid_view/spec/table_pagination.html +106 -0
- grid_view_spec/templates/grid_view/spec/tabs.html +47 -0
- grid_view_spec/templates/grid_view/spec/template.html +7 -0
- grid_view_spec/templates/grid_view/spec/toolbar.html +175 -0
- grid_view_spec/types/__init__.py +183 -0
- grid_view_spec/types/a2ui.py +53 -0
- grid_view_spec/types/actions.py +81 -0
- grid_view_spec/types/ag_grid.py +45 -0
- grid_view_spec/types/assets.py +19 -0
- grid_view_spec/types/block_base.py +60 -0
- grid_view_spec/types/blocks.py +42 -0
- grid_view_spec/types/chart_palette.py +21 -0
- grid_view_spec/types/chart_server.py +106 -0
- grid_view_spec/types/chart_wire.py +81 -0
- grid_view_spec/types/content.py +215 -0
- grid_view_spec/types/filters_v2.py +160 -0
- grid_view_spec/types/form.py +122 -0
- grid_view_spec/types/grid_settings.py +13 -0
- grid_view_spec/types/header.py +47 -0
- grid_view_spec/types/host.py +47 -0
- grid_view_spec/types/json.py +50 -0
- grid_view_spec/types/layout.py +68 -0
- grid_view_spec/types/lazy.py +55 -0
- grid_view_spec/types/media.py +56 -0
- grid_view_spec/types/narrowing.py +13 -0
- grid_view_spec/types/nav.py +29 -0
- grid_view_spec/types/numbers.py +47 -0
- grid_view_spec/types/overlay.py +30 -0
- grid_view_spec/types/result.py +46 -0
- grid_view_spec/types/semantic.py +43 -0
- grid_view_spec/types/spec.py +32 -0
- grid_view_spec/types/spec_meta.py +22 -0
- grid_view_spec/types/table_v2.py +219 -0
- grid_view_spec/types/toolbar.py +58 -0
- grid_view_spec/types/wire.py +37 -0
- grid_view_spec/validate/__init__.py +31 -0
- grid_view_spec/validate/chart_options.py +21 -0
- grid_view_spec/validate/codes.py +32 -0
- grid_view_spec/validate/normalize.py +37 -0
- grid_view_spec/validate/refs.py +139 -0
- grid_view_spec/validate/validate.py +706 -0
- grid_view_spec/validate/wire.py +155 -0
- grid_view_spec/wire_decode.py +1054 -0
- grid_view_spec-2.0.0.dist-info/METADATA +195 -0
- grid_view_spec-2.0.0.dist-info/RECORD +197 -0
- grid_view_spec-2.0.0.dist-info/WHEEL +5 -0
- grid_view_spec-2.0.0.dist-info/entry_points.txt +2 -0
- grid_view_spec-2.0.0.dist-info/licenses/LICENSE +21 -0
- grid_view_spec-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Framework-agnostic GridViewSpec vNext contract (grid-view-spec 2.x)."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from grid_view_spec.types import GridViewBlock, GridViewSpec, GridViewTable, GridViewToolbar
|
|
6
|
+
from grid_view_spec.validate import normalize_spec, spec_from_wire, spec_to_wire, validate_spec
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("grid-view-spec")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "2.0.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"GridViewBlock",
|
|
15
|
+
"GridViewSpec",
|
|
16
|
+
"GridViewTable",
|
|
17
|
+
"GridViewToolbar",
|
|
18
|
+
"__version__",
|
|
19
|
+
"normalize_spec",
|
|
20
|
+
"spec_from_wire",
|
|
21
|
+
"spec_to_wire",
|
|
22
|
+
"validate_spec",
|
|
23
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Django host backend adapters."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from grid_view_spec.backends.django.host import DjangoGridViewHost
|
|
6
|
+
from grid_view_spec.backends.django.prefs import DjangoOrmPrefs
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from grid_view_spec.backends.django.views import (
|
|
10
|
+
django_host,
|
|
11
|
+
export_pdf,
|
|
12
|
+
export_xlsx,
|
|
13
|
+
load_lazy_block,
|
|
14
|
+
render_lazy_block_response,
|
|
15
|
+
save_grid_prefs,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DjangoGridViewHost",
|
|
20
|
+
"DjangoOrmPrefs",
|
|
21
|
+
"django_host",
|
|
22
|
+
"export_pdf",
|
|
23
|
+
"export_xlsx",
|
|
24
|
+
"load_lazy_block",
|
|
25
|
+
"render_lazy_block_response",
|
|
26
|
+
"save_grid_prefs",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def __getattr__(name: str) -> object:
|
|
31
|
+
if name in {
|
|
32
|
+
"django_host",
|
|
33
|
+
"export_pdf",
|
|
34
|
+
"export_xlsx",
|
|
35
|
+
"load_lazy_block",
|
|
36
|
+
"render_lazy_block_response",
|
|
37
|
+
"save_grid_prefs",
|
|
38
|
+
}:
|
|
39
|
+
from grid_view_spec.backends.django import views
|
|
40
|
+
|
|
41
|
+
return getattr(views, name)
|
|
42
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
43
|
+
raise AttributeError(msg)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""AG-Grid infinite row model — server-side query helpers."""
|
|
2
|
+
|
|
3
|
+
from grid_view_spec.backends.django.ag_grid.export import (
|
|
4
|
+
EXPORT_COLS_PARAM,
|
|
5
|
+
parse_active_col_ids,
|
|
6
|
+
resolve_export_columns,
|
|
7
|
+
)
|
|
8
|
+
from grid_view_spec.backends.django.ag_grid.server import (
|
|
9
|
+
InfiniteGridParams,
|
|
10
|
+
apply_grid_filters,
|
|
11
|
+
apply_grid_sort,
|
|
12
|
+
parse_infinite_params,
|
|
13
|
+
)
|
|
14
|
+
from grid_view_spec.types.ag_grid import AgGridColumnSpec, AgGridPageSpec
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgGridColumnSpec",
|
|
18
|
+
"AgGridPageSpec",
|
|
19
|
+
"EXPORT_COLS_PARAM",
|
|
20
|
+
"InfiniteGridParams",
|
|
21
|
+
"apply_grid_filters",
|
|
22
|
+
"apply_grid_sort",
|
|
23
|
+
"parse_active_col_ids",
|
|
24
|
+
"parse_infinite_params",
|
|
25
|
+
"resolve_export_columns",
|
|
26
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Resolve AG-Grid export columns from page spec + optional live grid snapshot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.http import HttpRequest
|
|
6
|
+
from grid_view_spec.types.ag_grid import AgGridPageSpec
|
|
7
|
+
|
|
8
|
+
EXPORT_COLS_PARAM = "export_cols"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_active_col_ids(
|
|
12
|
+
request: HttpRequest,
|
|
13
|
+
*,
|
|
14
|
+
param: str = EXPORT_COLS_PARAM,
|
|
15
|
+
) -> tuple[str, ...]:
|
|
16
|
+
"""Read comma-separated active column ids synced from the browser grid."""
|
|
17
|
+
raw = request.GET.get(param, "")
|
|
18
|
+
return tuple(col_id.strip() for col_id in raw.split(",") if col_id.strip())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_export_columns(
|
|
22
|
+
spec: AgGridPageSpec,
|
|
23
|
+
request: HttpRequest,
|
|
24
|
+
) -> list[str]:
|
|
25
|
+
"""Resolve export column order from *spec* and optional ``export_cols`` query param."""
|
|
26
|
+
return spec.resolve_export_columns(parse_active_col_ids(request))
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Parse AG-Grid infinite-model request params and apply filters/sort to querysets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from django.db.models import F, Model, Q, QuerySet
|
|
11
|
+
from django.http import HttpRequest
|
|
12
|
+
from grid_view_spec.search.term_match import (
|
|
13
|
+
NUMERIC_OPS,
|
|
14
|
+
parse_range_bounds,
|
|
15
|
+
parse_search_number,
|
|
16
|
+
)
|
|
17
|
+
from grid_view_spec.types.json import as_str_object_dict, json_object_list_from
|
|
18
|
+
from grid_view_spec.types.narrowing import is_object_list
|
|
19
|
+
|
|
20
|
+
_QS = TypeVar("_QS", bound=QuerySet[Model])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class InfiniteGridParams:
|
|
25
|
+
"""Normalized query parameters from an AG-Grid infinite datasource fetch."""
|
|
26
|
+
|
|
27
|
+
start_row: int
|
|
28
|
+
end_row: int
|
|
29
|
+
search_query: str
|
|
30
|
+
visible_cols: tuple[str, ...]
|
|
31
|
+
filters: dict[str, object]
|
|
32
|
+
sort_model: list[dict[str, object]]
|
|
33
|
+
action: str | None = None
|
|
34
|
+
action_field: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_infinite_params(
|
|
38
|
+
request: HttpRequest,
|
|
39
|
+
*,
|
|
40
|
+
default_page_size: int = 100,
|
|
41
|
+
) -> InfiniteGridParams:
|
|
42
|
+
"""Read standard infinite-grid GET params from *request*."""
|
|
43
|
+
start_row = int(request.GET.get("startRow", 0))
|
|
44
|
+
end_row = int(request.GET.get("endRow", default_page_size))
|
|
45
|
+
search_query = request.GET.get("q", "").strip()
|
|
46
|
+
visible_cols_str = request.GET.get("cols", "")
|
|
47
|
+
visible_cols = tuple(c.strip() for c in visible_cols_str.split(",") if c.strip())
|
|
48
|
+
|
|
49
|
+
filters: dict[str, object] = {}
|
|
50
|
+
filters_json = request.GET.get("filters")
|
|
51
|
+
if filters_json:
|
|
52
|
+
try:
|
|
53
|
+
filters = as_str_object_dict(json.loads(filters_json))
|
|
54
|
+
except (json.JSONDecodeError, TypeError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
sort_model: list[dict[str, object]] = []
|
|
58
|
+
sort_json = request.GET.get("sort")
|
|
59
|
+
if sort_json:
|
|
60
|
+
try:
|
|
61
|
+
parsed_sort = json.loads(sort_json)
|
|
62
|
+
sort_model = [
|
|
63
|
+
{str(k): v for k, v in item.items()} for item in json_object_list_from(parsed_sort)
|
|
64
|
+
]
|
|
65
|
+
except (json.JSONDecodeError, TypeError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
action = request.GET.get("action") or None
|
|
69
|
+
action_field = request.GET.get("field") or None
|
|
70
|
+
|
|
71
|
+
return InfiniteGridParams(
|
|
72
|
+
start_row=start_row,
|
|
73
|
+
end_row=end_row,
|
|
74
|
+
search_query=search_query,
|
|
75
|
+
visible_cols=visible_cols,
|
|
76
|
+
filters=filters,
|
|
77
|
+
sort_model=sort_model,
|
|
78
|
+
action=action,
|
|
79
|
+
action_field=action_field,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
FilterValueFormatter = Callable[[str, object], object]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def apply_grid_filters(
|
|
87
|
+
qs: _QS,
|
|
88
|
+
filters: Mapping[str, object],
|
|
89
|
+
field_map: Mapping[str, str],
|
|
90
|
+
*,
|
|
91
|
+
format_filter_value: FilterValueFormatter | None = None,
|
|
92
|
+
) -> _QS:
|
|
93
|
+
"""Apply AG-Grid ``filterModel`` entries to *qs* using *field_map* (colId → ORM path)."""
|
|
94
|
+
for col_id, rules in filters.items():
|
|
95
|
+
if col_id not in field_map:
|
|
96
|
+
continue
|
|
97
|
+
rule_map = as_str_object_dict(rules)
|
|
98
|
+
if not rule_map:
|
|
99
|
+
continue
|
|
100
|
+
db_field = field_map[col_id]
|
|
101
|
+
|
|
102
|
+
mode = rule_map.get("mode")
|
|
103
|
+
if mode == "empty":
|
|
104
|
+
qs = qs.filter(empty_field_q(db_field, numeric=bool(rule_map.get("numeric"))))
|
|
105
|
+
continue
|
|
106
|
+
if mode == "non_empty":
|
|
107
|
+
qs = qs.exclude(empty_field_q(db_field, numeric=bool(rule_map.get("numeric"))))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
values = rule_map.get("values")
|
|
111
|
+
if is_object_list(values):
|
|
112
|
+
target_vals: list[object] = []
|
|
113
|
+
for val in values:
|
|
114
|
+
if format_filter_value is not None:
|
|
115
|
+
target_vals.append(format_filter_value(col_id, val))
|
|
116
|
+
else:
|
|
117
|
+
target_vals.append(val)
|
|
118
|
+
qs = qs.filter(**{f"{db_field}__in": target_vals})
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if rule_map.get("filterType") == "cm-expr":
|
|
122
|
+
expr = str(rule_map.get("expr", "")).strip()
|
|
123
|
+
if expr:
|
|
124
|
+
expr_q = build_expr_q(db_field, expr, numeric=bool(rule_map.get("numeric")))
|
|
125
|
+
if expr_q is not None:
|
|
126
|
+
qs = qs.filter(expr_q)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if rule_map.get("filterType") == "text":
|
|
130
|
+
operator = str(rule_map.get("type", "contains"))
|
|
131
|
+
f_val = rule_map.get("filter", "")
|
|
132
|
+
if not f_val:
|
|
133
|
+
continue
|
|
134
|
+
lookup = _text_lookup(db_field, operator)
|
|
135
|
+
if lookup is None:
|
|
136
|
+
continue
|
|
137
|
+
if operator == "notContains":
|
|
138
|
+
qs = qs.exclude(**{lookup: f_val})
|
|
139
|
+
elif operator == "notEqual":
|
|
140
|
+
qs = qs.exclude(**{lookup: f_val})
|
|
141
|
+
else:
|
|
142
|
+
qs = qs.filter(**{lookup: f_val})
|
|
143
|
+
return qs
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def apply_grid_sort(
|
|
147
|
+
qs: _QS,
|
|
148
|
+
sort_model: Sequence[Mapping[str, object]],
|
|
149
|
+
field_map: Mapping[str, str],
|
|
150
|
+
*,
|
|
151
|
+
tie_breaker: str = "-id",
|
|
152
|
+
) -> _QS:
|
|
153
|
+
"""Apply the first AG-Grid sort model entry to *qs*."""
|
|
154
|
+
if not sort_model:
|
|
155
|
+
return qs
|
|
156
|
+
first = sort_model[0]
|
|
157
|
+
ui_col = str(first.get("colId", ""))
|
|
158
|
+
direction = str(first.get("sort", "asc"))
|
|
159
|
+
db_field = field_map.get(ui_col)
|
|
160
|
+
if not db_field:
|
|
161
|
+
return qs
|
|
162
|
+
f_obj = F(db_field)
|
|
163
|
+
if direction == "desc":
|
|
164
|
+
f_obj = f_obj.desc(nulls_last=True)
|
|
165
|
+
else:
|
|
166
|
+
f_obj = f_obj.asc(nulls_last=True)
|
|
167
|
+
return qs.order_by(f_obj, tie_breaker)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def empty_field_q(db_field: str, *, numeric: bool = False) -> Q:
|
|
171
|
+
"""Values treated as empty: null always; 0 for numeric columns, else blank/dash.
|
|
172
|
+
|
|
173
|
+
Comparing a numeric column against ``""``/``"-"`` raises a DB type error, so
|
|
174
|
+
numeric columns use ``isnull`` + ``= 0`` instead.
|
|
175
|
+
"""
|
|
176
|
+
if numeric:
|
|
177
|
+
return Q(**{f"{db_field}__isnull": True}) | Q(**{db_field: 0})
|
|
178
|
+
return Q(**{f"{db_field}__isnull": True}) | Q(**{db_field: ""}) | Q(**{db_field: "-"})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _text_lookup(db_field: str, operator: str) -> str | None:
|
|
182
|
+
mapping = {
|
|
183
|
+
"contains": f"{db_field}__icontains",
|
|
184
|
+
"notContains": f"{db_field}__icontains",
|
|
185
|
+
"equals": f"{db_field}__iexact",
|
|
186
|
+
"notEqual": f"{db_field}__iexact",
|
|
187
|
+
"startsWith": f"{db_field}__istartswith",
|
|
188
|
+
"endsWith": f"{db_field}__iendswith",
|
|
189
|
+
}
|
|
190
|
+
return mapping.get(operator)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
_NUMERIC_OP_LOOKUPS = {">": "__gt", ">=": "__gte", "<": "__lt", "<=": "__lte", "=": ""}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_expr_q(db_field: str, expr: str, *, numeric: bool = False) -> Q | None:
|
|
197
|
+
"""Translate a smart-search expression into an ORM ``Q`` for *db_field*.
|
|
198
|
+
|
|
199
|
+
Mirrors the client matcher (``grid_view_spec.search.term_match``). Numeric
|
|
200
|
+
comparison/range lookups are emitted only for ``numeric`` columns to avoid
|
|
201
|
+
type-mismatch errors when comparing a text column against a number.
|
|
202
|
+
"""
|
|
203
|
+
q = str(expr or "").strip()
|
|
204
|
+
if not q:
|
|
205
|
+
return None
|
|
206
|
+
# Negation: !<expr>
|
|
207
|
+
if len(q) > 1 and q[0] == "!":
|
|
208
|
+
inner = build_expr_q(db_field, q[1:].strip(), numeric=numeric)
|
|
209
|
+
return ~inner if inner is not None else None
|
|
210
|
+
if numeric:
|
|
211
|
+
bounds = parse_range_bounds(q)
|
|
212
|
+
if bounds is not None:
|
|
213
|
+
return Q(**{f"{db_field}__gte": bounds[0], f"{db_field}__lte": bounds[1]})
|
|
214
|
+
for op in NUMERIC_OPS:
|
|
215
|
+
if q.startswith(op):
|
|
216
|
+
num = parse_search_number(q[len(op) :].strip())
|
|
217
|
+
if num is None:
|
|
218
|
+
return None
|
|
219
|
+
suffix = _NUMERIC_OP_LOOKUPS[op]
|
|
220
|
+
return Q(**{db_field if suffix == "" else f"{db_field}{suffix}": num})
|
|
221
|
+
num = parse_search_number(q)
|
|
222
|
+
return Q(**{db_field: num}) if num is not None else None
|
|
223
|
+
# Text operators (safe on text columns).
|
|
224
|
+
if len(q) > 1 and q[0] == "^":
|
|
225
|
+
return Q(**{f"{db_field}__istartswith": q[1:].strip()})
|
|
226
|
+
if len(q) > 1 and q[-1] == "$":
|
|
227
|
+
return Q(**{f"{db_field}__iendswith": q[:-1].strip()})
|
|
228
|
+
if "%" in q:
|
|
229
|
+
if q.startswith("%") and q.endswith("%") and len(q) >= 2:
|
|
230
|
+
return Q(**{f"{db_field}__icontains": q[1:-1]})
|
|
231
|
+
if q.startswith("%"):
|
|
232
|
+
return Q(**{f"{db_field}__iendswith": q[1:]})
|
|
233
|
+
if q.endswith("%"):
|
|
234
|
+
return Q(**{f"{db_field}__istartswith": q[:-1]})
|
|
235
|
+
return Q(**{f"{db_field}__icontains": q})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import grid_view_spec
|
|
4
|
+
from django.apps import AppConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GridViewSpecDjangoConfig(AppConfig):
|
|
8
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
9
|
+
name = "grid_view_spec.backends.django"
|
|
10
|
+
label = "grid_view_spec_django"
|
|
11
|
+
path = str(Path(grid_view_spec.__file__).resolve().parent)
|
|
12
|
+
verbose_name = "Grid View Spec (Django)"
|
|
13
|
+
|
|
14
|
+
def ready(self) -> None:
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from grid_view_spec.backends.django.wire_templates import wire_grid_view_spec_templates
|
|
17
|
+
|
|
18
|
+
wire_grid_view_spec_templates(settings.TEMPLATES)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Django asset plan → ``<link>`` / ``<script>`` emission for GridViewSpec pages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.utils.safestring import SafeString, mark_safe
|
|
6
|
+
from grid_view_spec.render.block_registry import (
|
|
7
|
+
AG_GRID_BUNDLE_IDS,
|
|
8
|
+
CHARTS_BUNDLE_ID,
|
|
9
|
+
CORE_BUNDLE_ID,
|
|
10
|
+
blocks_require_ag_grid,
|
|
11
|
+
blocks_require_charts,
|
|
12
|
+
)
|
|
13
|
+
from grid_view_spec.types.spec import GridViewSpec
|
|
14
|
+
|
|
15
|
+
# Request flags set by ``render_grid_view_spec``; read by ``grid_view_spec_assets``.
|
|
16
|
+
AG_GRID_REQUEST_FLAG = "_grid_view_spec_needs_ag_grid"
|
|
17
|
+
CHARTS_REQUEST_FLAG = "_grid_view_spec_needs_charts"
|
|
18
|
+
|
|
19
|
+
_ASSETS_CSS_FLAG = "_grid_view_spec_assets_css_emitted"
|
|
20
|
+
_ASSETS_JS_FLAG = "_grid_view_spec_assets_js_emitted"
|
|
21
|
+
_AG_GRID_JS_SUPPLEMENT_FLAG = "_grid_view_spec_ag_grid_js_emitted"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mark_ag_grid_needed(request: object) -> None:
|
|
25
|
+
setattr(request, AG_GRID_REQUEST_FLAG, True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mark_charts_needed(request: object) -> None:
|
|
29
|
+
setattr(request, CHARTS_REQUEST_FLAG, True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def mark_assets_from_spec(request: object, spec: GridViewSpec) -> None:
|
|
33
|
+
"""Mark optional bundle requirements on ``request`` before template ``<head>`` renders."""
|
|
34
|
+
if blocks_require_ag_grid(spec.blocks):
|
|
35
|
+
mark_ag_grid_needed(request)
|
|
36
|
+
if blocks_require_charts(spec.blocks):
|
|
37
|
+
mark_charts_needed(request)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _request_flag(request: object | None, name: str) -> bool:
|
|
41
|
+
if request is None:
|
|
42
|
+
return False
|
|
43
|
+
return bool(getattr(request, name, False))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_assets_context(
|
|
47
|
+
request: object | None,
|
|
48
|
+
*,
|
|
49
|
+
part: str,
|
|
50
|
+
force_core: bool = False,
|
|
51
|
+
force_ag_grid: bool = False,
|
|
52
|
+
force_charts: bool = False,
|
|
53
|
+
) -> dict[str, SafeString | str | bool | tuple[str, ...]]:
|
|
54
|
+
"""Build inclusion-tag context for CSS or JS asset partials."""
|
|
55
|
+
from grid_view_spec.backends.django.conf import (
|
|
56
|
+
ag_grid_cdn_url,
|
|
57
|
+
ag_grid_stylesheet_urls,
|
|
58
|
+
ag_grid_stylesheets_bundled,
|
|
59
|
+
echarts_cdn_url,
|
|
60
|
+
)
|
|
61
|
+
from grid_view_spec.backends.django.grid_preferences import grid_preferences_url
|
|
62
|
+
from grid_view_spec.backends.django.i18n import get_js_i18n_catalog_json
|
|
63
|
+
|
|
64
|
+
if part == "css":
|
|
65
|
+
if request is not None and getattr(request, _ASSETS_CSS_FLAG, False):
|
|
66
|
+
return {
|
|
67
|
+
"include": False,
|
|
68
|
+
"load_ag_grid": False,
|
|
69
|
+
"ag_grid_css_url": "",
|
|
70
|
+
"ag_grid_theme_css_url": "",
|
|
71
|
+
}
|
|
72
|
+
if request is not None:
|
|
73
|
+
setattr(request, _ASSETS_CSS_FLAG, True)
|
|
74
|
+
load_ag_grid_css = force_ag_grid or _request_flag(request, AG_GRID_REQUEST_FLAG)
|
|
75
|
+
ag_grid_css, ag_grid_theme_css = ("", "")
|
|
76
|
+
if load_ag_grid_css:
|
|
77
|
+
from grid_view_spec.backends.django.conf import ag_grid_stylesheet_urls
|
|
78
|
+
|
|
79
|
+
ag_grid_css, ag_grid_theme_css = ag_grid_stylesheet_urls()
|
|
80
|
+
return {
|
|
81
|
+
"include": True,
|
|
82
|
+
"load_ag_grid": load_ag_grid_css,
|
|
83
|
+
"ag_grid_bundled": ag_grid_stylesheets_bundled(),
|
|
84
|
+
"ag_grid_css_url": ag_grid_css,
|
|
85
|
+
"ag_grid_theme_css_url": ag_grid_theme_css,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if part == "ag_grid":
|
|
89
|
+
load_ag_grid = force_ag_grid or _request_flag(request, AG_GRID_REQUEST_FLAG)
|
|
90
|
+
empty_ag_grid: dict[str, SafeString | str | bool | tuple[str, ...]] = {
|
|
91
|
+
"include_scripts": False,
|
|
92
|
+
"load_ag_grid": False,
|
|
93
|
+
"ag_grid_bundled": False,
|
|
94
|
+
"ag_grid_cdn_url": "",
|
|
95
|
+
}
|
|
96
|
+
if not load_ag_grid:
|
|
97
|
+
return empty_ag_grid
|
|
98
|
+
if request is not None and getattr(request, _AG_GRID_JS_SUPPLEMENT_FLAG, False):
|
|
99
|
+
return empty_ag_grid
|
|
100
|
+
if request is not None:
|
|
101
|
+
setattr(request, _AG_GRID_JS_SUPPLEMENT_FLAG, True)
|
|
102
|
+
return {
|
|
103
|
+
"include_scripts": True,
|
|
104
|
+
"load_ag_grid": True,
|
|
105
|
+
"ag_grid_bundled": ag_grid_stylesheets_bundled(),
|
|
106
|
+
"ag_grid_cdn_url": ag_grid_cdn_url(),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if request is not None and getattr(request, _ASSETS_JS_FLAG, False):
|
|
110
|
+
return {
|
|
111
|
+
"include_scripts": False,
|
|
112
|
+
"load_charts": False,
|
|
113
|
+
"load_ag_grid": False,
|
|
114
|
+
"grid_view_i18n_catalog": mark_safe("{}"),
|
|
115
|
+
"preferences_url": grid_preferences_url(),
|
|
116
|
+
"ag_grid_cdn_url": "",
|
|
117
|
+
"echarts_cdn_url": "",
|
|
118
|
+
"core_bundle": CORE_BUNDLE_ID,
|
|
119
|
+
"charts_bundle": CHARTS_BUNDLE_ID,
|
|
120
|
+
"ag_grid_bundles": (),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
load_ag_grid = force_ag_grid or _request_flag(request, AG_GRID_REQUEST_FLAG)
|
|
124
|
+
load_charts = force_charts or _request_flag(request, CHARTS_REQUEST_FLAG)
|
|
125
|
+
include_scripts = force_core or request is not None or load_ag_grid or load_charts
|
|
126
|
+
from grid_view_spec.backends.django.conf import ag_grid_stylesheet_urls
|
|
127
|
+
|
|
128
|
+
ag_grid_css, ag_grid_theme_css = ag_grid_stylesheet_urls()
|
|
129
|
+
|
|
130
|
+
if request is not None:
|
|
131
|
+
setattr(request, _ASSETS_JS_FLAG, True)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"include_scripts": include_scripts,
|
|
135
|
+
"load_charts": load_charts,
|
|
136
|
+
"load_ag_grid": load_ag_grid,
|
|
137
|
+
# Selects static-vs-CDN URLs in the __GridViewAssets manifest. Must reflect
|
|
138
|
+
# whether vendor assets are bundled regardless of load_ag_grid: the manifest
|
|
139
|
+
# is consumed by the client lazy-loader after HTMX swaps, when AG-Grid was
|
|
140
|
+
# not eagerly loaded this render. Gating on load_ag_grid emitted raw relative
|
|
141
|
+
# vendor paths that resolved against the page URL (NS_ERROR_CORRUPTED_CONTENT).
|
|
142
|
+
"ag_grid_bundled": ag_grid_stylesheets_bundled(),
|
|
143
|
+
"grid_view_i18n_catalog": mark_safe(get_js_i18n_catalog_json()),
|
|
144
|
+
"preferences_url": grid_preferences_url(),
|
|
145
|
+
"ag_grid_cdn_url": ag_grid_cdn_url(),
|
|
146
|
+
"echarts_cdn_url": echarts_cdn_url(),
|
|
147
|
+
"ag_grid_css_url": ag_grid_css,
|
|
148
|
+
"ag_grid_theme_css_url": ag_grid_theme_css,
|
|
149
|
+
"core_bundle": CORE_BUNDLE_ID,
|
|
150
|
+
"charts_bundle": CHARTS_BUNDLE_ID,
|
|
151
|
+
"ag_grid_bundles": AG_GRID_BUNDLE_IDS if load_ag_grid else (),
|
|
152
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Host-configurable URL names and CDN pins for third-party scripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
DEFAULT_AG_GRID_VERSION = "31.3.4"
|
|
8
|
+
DEFAULT_SORTABLE_VERSION = "1.15.2"
|
|
9
|
+
DEFAULT_ECHARTS_VERSION = "5.5.1"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _str_setting(name: str, default: str) -> str:
|
|
13
|
+
val = getattr(settings, name, None)
|
|
14
|
+
if val is not None:
|
|
15
|
+
return str(val).strip()
|
|
16
|
+
return default
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _optional_str_setting(name: str) -> str | None:
|
|
20
|
+
val = getattr(settings, name, None)
|
|
21
|
+
if val is not None:
|
|
22
|
+
return str(val).strip()
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def export_pdf_url_name() -> str:
|
|
27
|
+
"""Django URL name for PDF export (default ``api_export_pdf``)."""
|
|
28
|
+
return _str_setting("GRID_VIEW_SPEC_EXPORT_PDF_URL", "api_export_pdf")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def grid_preferences_url_name() -> str:
|
|
32
|
+
"""Django URL name for saved column presets (default ``api_grid_preferences``)."""
|
|
33
|
+
return _str_setting("GRID_VIEW_SPEC_GRID_PREFERENCES_URL", "api_grid_preferences")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def export_xlsx_url_name() -> str:
|
|
37
|
+
"""Django URL name for XLSX export (default ``api_export_xlsx``)."""
|
|
38
|
+
return _str_setting("GRID_VIEW_SPEC_EXPORT_XLSX_URL", "api_export_xlsx")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def lazy_url_name() -> str:
|
|
42
|
+
"""Django URL name for HTMX lazy block loader (default ``lazy``)."""
|
|
43
|
+
return _str_setting("GRID_VIEW_SPEC_LAZY_URL", "lazy")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ag_grid_version() -> str:
|
|
47
|
+
"""Pinned AG-Grid Community semver (default ``31.3.4``)."""
|
|
48
|
+
return _str_setting("GRID_VIEW_SPEC_AG_GRID_VERSION", DEFAULT_AG_GRID_VERSION)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ag_grid_cdn_url() -> str:
|
|
52
|
+
"""Script URL for AG-Grid Community.
|
|
53
|
+
|
|
54
|
+
Default: vendored copy under ``grid_view_spec/vendor/`` (see frontend build).
|
|
55
|
+
Override with ``GRID_VIEW_SPEC_AG_GRID_CDN_URL``.
|
|
56
|
+
"""
|
|
57
|
+
override = _optional_str_setting("GRID_VIEW_SPEC_AG_GRID_CDN_URL")
|
|
58
|
+
if override:
|
|
59
|
+
return override
|
|
60
|
+
return "grid_view_spec/vendor/ag-grid-community.min.js"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ag_grid_use_bundled_assets() -> bool:
|
|
64
|
+
"""True when templates should emit ``{% static %}`` vendor paths."""
|
|
65
|
+
return _optional_str_setting("GRID_VIEW_SPEC_AG_GRID_CDN_URL") is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ag_grid_stylesheet_urls() -> tuple[str, str]:
|
|
69
|
+
"""AG-Grid core + Quartz theme CSS URLs."""
|
|
70
|
+
css_override = _optional_str_setting("GRID_VIEW_SPEC_AG_GRID_CSS_URL")
|
|
71
|
+
theme_override = _optional_str_setting("GRID_VIEW_SPEC_AG_GRID_THEME_CSS_URL")
|
|
72
|
+
if css_override and theme_override:
|
|
73
|
+
return (css_override, theme_override)
|
|
74
|
+
if ag_grid_use_bundled_assets():
|
|
75
|
+
return (
|
|
76
|
+
"grid_view_spec/vendor/ag-grid.css",
|
|
77
|
+
"grid_view_spec/vendor/ag-theme-quartz.css",
|
|
78
|
+
)
|
|
79
|
+
version = ag_grid_version()
|
|
80
|
+
base = f"https://cdn.jsdelivr.net/npm/ag-grid-community@{version}/styles"
|
|
81
|
+
return (f"{base}/ag-grid.css", f"{base}/ag-theme-quartz.css")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def ag_grid_stylesheets_bundled() -> bool:
|
|
85
|
+
"""When True, ``css.html`` / ``js.html`` use ``{% static %}`` vendor paths."""
|
|
86
|
+
return (
|
|
87
|
+
ag_grid_use_bundled_assets()
|
|
88
|
+
and _optional_str_setting("GRID_VIEW_SPEC_AG_GRID_CSS_URL") is None
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sortable_version() -> str:
|
|
93
|
+
"""Pinned SortableJS semver for column-settings drag-reorder (default ``1.15.2``)."""
|
|
94
|
+
return _str_setting("GRID_VIEW_SPEC_SORTABLE_VERSION", DEFAULT_SORTABLE_VERSION)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def sortable_cdn_url() -> str:
|
|
98
|
+
"""Script URL for SortableJS (column settings modal)."""
|
|
99
|
+
override = _optional_str_setting("GRID_VIEW_SPEC_SORTABLE_CDN_URL")
|
|
100
|
+
if override:
|
|
101
|
+
return override
|
|
102
|
+
version = sortable_version()
|
|
103
|
+
return f"https://cdn.jsdelivr.net/npm/sortablejs@{version}/Sortable.min.js"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def echarts_version() -> str:
|
|
107
|
+
"""Pinned ECharts semver for host base templates (default ``5.5.1``)."""
|
|
108
|
+
return _str_setting("GRID_VIEW_SPEC_ECHARTS_VERSION", DEFAULT_ECHARTS_VERSION)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def echarts_cdn_url() -> str:
|
|
112
|
+
"""Script URL for Apache ECharts (load in host base template before page JS bundle)."""
|
|
113
|
+
override = _optional_str_setting("GRID_VIEW_SPEC_ECHARTS_CDN_URL")
|
|
114
|
+
if override:
|
|
115
|
+
return override
|
|
116
|
+
version = echarts_version()
|
|
117
|
+
return f"https://cdn.jsdelivr.net/npm/echarts@{version}/dist/echarts.min.js"
|