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.
Files changed (197) hide show
  1. grid_view_spec/__init__.py +23 -0
  2. grid_view_spec/backends/django/__init__.py +43 -0
  3. grid_view_spec/backends/django/ag_grid/__init__.py +26 -0
  4. grid_view_spec/backends/django/ag_grid/export.py +26 -0
  5. grid_view_spec/backends/django/ag_grid/server.py +235 -0
  6. grid_view_spec/backends/django/apps.py +18 -0
  7. grid_view_spec/backends/django/assets.py +152 -0
  8. grid_view_spec/backends/django/conf.py +117 -0
  9. grid_view_spec/backends/django/export.py +89 -0
  10. grid_view_spec/backends/django/facets.py +68 -0
  11. grid_view_spec/backends/django/grid_preferences.py +16 -0
  12. grid_view_spec/backends/django/host.py +88 -0
  13. grid_view_spec/backends/django/hrefs.py +30 -0
  14. grid_view_spec/backends/django/i18n.py +24 -0
  15. grid_view_spec/backends/django/lazy.py +60 -0
  16. grid_view_spec/backends/django/migrations/0001_initial.py +33 -0
  17. grid_view_spec/backends/django/migrations/__init__.py +0 -0
  18. grid_view_spec/backends/django/models.py +26 -0
  19. grid_view_spec/backends/django/pdf_response.py +16 -0
  20. grid_view_spec/backends/django/prefs.py +53 -0
  21. grid_view_spec/backends/django/queryset_search.py +112 -0
  22. grid_view_spec/backends/django/search.py +47 -0
  23. grid_view_spec/backends/django/templatetags.py +119 -0
  24. grid_view_spec/backends/django/throttle.py +111 -0
  25. grid_view_spec/backends/django/urls.py +35 -0
  26. grid_view_spec/backends/django/views.py +242 -0
  27. grid_view_spec/backends/django/wire_templates.py +31 -0
  28. grid_view_spec/backends/fastapi/router.py +38 -0
  29. grid_view_spec/backends/fragment/html.py +107 -0
  30. grid_view_spec/backends/jinja2/renderer.py +21 -0
  31. grid_view_spec/backends/json/__init__.py +5 -0
  32. grid_view_spec/backends/json/wire.py +59 -0
  33. grid_view_spec/backends/starlette/host.py +23 -0
  34. grid_view_spec/backends/starlette/routes.py +31 -0
  35. grid_view_spec/backends/starlette/static.py +38 -0
  36. grid_view_spec/export/__init__.py +49 -0
  37. grid_view_spec/export/charts_png.py +51 -0
  38. grid_view_spec/export/columns.py +90 -0
  39. grid_view_spec/export/context.py +124 -0
  40. grid_view_spec/export/html.py +72 -0
  41. grid_view_spec/export/meta.py +120 -0
  42. grid_view_spec/export/payload.py +79 -0
  43. grid_view_spec/export/pdf/backends.py +44 -0
  44. grid_view_spec/export/pipeline.py +131 -0
  45. grid_view_spec/export/print.py +161 -0
  46. grid_view_spec/export/registry.py +100 -0
  47. grid_view_spec/export/rows.py +103 -0
  48. grid_view_spec/export/static_charts.py +216 -0
  49. grid_view_spec/export/xlsx.py +286 -0
  50. grid_view_spec/hosts/__init__.py +20 -0
  51. grid_view_spec/hosts/base.py +17 -0
  52. grid_view_spec/hosts/memory.py +77 -0
  53. grid_view_spec/i18n.py +118 -0
  54. grid_view_spec/locale/en/LC_MESSAGES/django.mo +0 -0
  55. grid_view_spec/locale/en/LC_MESSAGES/django.po +324 -0
  56. grid_view_spec/locale/uk/LC_MESSAGES/django.mo +0 -0
  57. grid_view_spec/locale/uk/LC_MESSAGES/django.po +330 -0
  58. grid_view_spec/mcp/__init__.py +23 -0
  59. grid_view_spec/mcp/__main__.py +10 -0
  60. grid_view_spec/mcp/a2ui.py +337 -0
  61. grid_view_spec/mcp/catalog.py +388 -0
  62. grid_view_spec/mcp/envelope.py +64 -0
  63. grid_view_spec/mcp/examples.py +131 -0
  64. grid_view_spec/mcp/fixtures.py +1188 -0
  65. grid_view_spec/mcp/normalize.py +62 -0
  66. grid_view_spec/mcp/schema.py +138 -0
  67. grid_view_spec/mcp/server.py +158 -0
  68. grid_view_spec/mcp/validate.py +48 -0
  69. grid_view_spec/py.typed +1 -0
  70. grid_view_spec/render/__init__.py +16 -0
  71. grid_view_spec/render/a2ui.py +277 -0
  72. grid_view_spec/render/action_urls.py +218 -0
  73. grid_view_spec/render/ag_grid.py +92 -0
  74. grid_view_spec/render/bind.py +101 -0
  75. grid_view_spec/render/block_registry.py +103 -0
  76. grid_view_spec/render/chart_runtime.py +51 -0
  77. grid_view_spec/render/chart_spec.py +97 -0
  78. grid_view_spec/render/charts_bind.py +265 -0
  79. grid_view_spec/render/column_settings.py +137 -0
  80. grid_view_spec/render/context.py +43 -0
  81. grid_view_spec/render/jinja.py +31 -0
  82. grid_view_spec/render/lazy.py +25 -0
  83. grid_view_spec/render/refs.py +25 -0
  84. grid_view_spec/render/request_state.py +109 -0
  85. grid_view_spec/render/row_template.py +53 -0
  86. grid_view_spec/render/spec_renderer.py +335 -0
  87. grid_view_spec/render/tab_panes.py +52 -0
  88. grid_view_spec/render/table_chart.py +36 -0
  89. grid_view_spec/render/table_edit.py +56 -0
  90. grid_view_spec/schema/grid-view-spec.v2.json +871 -0
  91. grid_view_spec/search/__init__.py +7 -0
  92. grid_view_spec/search/column.py +35 -0
  93. grid_view_spec/search/column_meta.py +35 -0
  94. grid_view_spec/search/column_scope.py +82 -0
  95. grid_view_spec/search/contract.py +318 -0
  96. grid_view_spec/search/engine.py +223 -0
  97. grid_view_spec/search/facet_registry.py +91 -0
  98. grid_view_spec/search/facets.py +166 -0
  99. grid_view_spec/search/params.py +8 -0
  100. grid_view_spec/search/query_commit.py +28 -0
  101. grid_view_spec/search/smart.py +319 -0
  102. grid_view_spec/search/syntax_tips.py +83 -0
  103. grid_view_spec/search/term_match.py +197 -0
  104. grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-cdn.js +16 -0
  105. grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-cdn.min.js +1 -0
  106. grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-theme.css +250 -0
  107. grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid-theme.min.css +1 -0
  108. grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid.js +1965 -0
  109. grid_view_spec/static/grid_view_spec/gridviewspec-ag-grid.min.js +1 -0
  110. grid_view_spec/static/grid_view_spec/gridviewspec-charts.js +577 -0
  111. grid_view_spec/static/grid_view_spec/gridviewspec-charts.min.js +3 -0
  112. grid_view_spec/static/grid_view_spec/gridviewspec.css +3662 -0
  113. grid_view_spec/static/grid_view_spec/gridviewspec.js +7000 -0
  114. grid_view_spec/static/grid_view_spec/gridviewspec.min.css +1 -0
  115. grid_view_spec/static/grid_view_spec/gridviewspec.min.js +3 -0
  116. grid_view_spec/static/grid_view_spec/vendor/ag-grid-community.min.js +1 -0
  117. grid_view_spec/static/grid_view_spec/vendor/ag-grid.css +8644 -0
  118. grid_view_spec/static/grid_view_spec/vendor/ag-theme-quartz.css +865 -0
  119. grid_view_spec/templates/export/base.html +67 -0
  120. grid_view_spec/templates/export/blocks/charts.html +7 -0
  121. grid_view_spec/templates/export/blocks/table.html +60 -0
  122. grid_view_spec/templates/export/spec_report.html +12 -0
  123. grid_view_spec/templates/grid_view/assets/ag_grid_js.html +9 -0
  124. grid_view_spec/templates/grid_view/assets/css.html +15 -0
  125. grid_view_spec/templates/grid_view/assets/js.html +54 -0
  126. grid_view_spec/templates/grid_view/spec/_action_button.html +12 -0
  127. grid_view_spec/templates/grid_view/spec/_action_link.html +12 -0
  128. grid_view_spec/templates/grid_view/spec/_column_settings_panel.html +25 -0
  129. grid_view_spec/templates/grid_view/spec/_filter_bar.html +57 -0
  130. grid_view_spec/templates/grid_view/spec/_icon.html +10 -0
  131. grid_view_spec/templates/grid_view/spec/_semantic.html +17 -0
  132. grid_view_spec/templates/grid_view/spec/actions.html +36 -0
  133. grid_view_spec/templates/grid_view/spec/block.html +30 -0
  134. grid_view_spec/templates/grid_view/spec/card_groups.html +17 -0
  135. grid_view_spec/templates/grid_view/spec/cards.html +38 -0
  136. grid_view_spec/templates/grid_view/spec/charts.html +15 -0
  137. grid_view_spec/templates/grid_view/spec/content.html +15 -0
  138. grid_view_spec/templates/grid_view/spec/filters.html +4 -0
  139. grid_view_spec/templates/grid_view/spec/form.html +5 -0
  140. grid_view_spec/templates/grid_view/spec/gallery.html +5 -0
  141. grid_view_spec/templates/grid_view/spec/header.html +60 -0
  142. grid_view_spec/templates/grid_view/spec/image.html +3 -0
  143. grid_view_spec/templates/grid_view/spec/kpi.html +14 -0
  144. grid_view_spec/templates/grid_view/spec/lazy_placeholder.html +9 -0
  145. grid_view_spec/templates/grid_view/spec/nav.html +5 -0
  146. grid_view_spec/templates/grid_view/spec/overlay.html +3 -0
  147. grid_view_spec/templates/grid_view/spec/spec.html +46 -0
  148. grid_view_spec/templates/grid_view/spec/table.html +336 -0
  149. grid_view_spec/templates/grid_view/spec/table_pagination.html +106 -0
  150. grid_view_spec/templates/grid_view/spec/tabs.html +47 -0
  151. grid_view_spec/templates/grid_view/spec/template.html +7 -0
  152. grid_view_spec/templates/grid_view/spec/toolbar.html +175 -0
  153. grid_view_spec/types/__init__.py +183 -0
  154. grid_view_spec/types/a2ui.py +53 -0
  155. grid_view_spec/types/actions.py +81 -0
  156. grid_view_spec/types/ag_grid.py +45 -0
  157. grid_view_spec/types/assets.py +19 -0
  158. grid_view_spec/types/block_base.py +60 -0
  159. grid_view_spec/types/blocks.py +42 -0
  160. grid_view_spec/types/chart_palette.py +21 -0
  161. grid_view_spec/types/chart_server.py +106 -0
  162. grid_view_spec/types/chart_wire.py +81 -0
  163. grid_view_spec/types/content.py +215 -0
  164. grid_view_spec/types/filters_v2.py +160 -0
  165. grid_view_spec/types/form.py +122 -0
  166. grid_view_spec/types/grid_settings.py +13 -0
  167. grid_view_spec/types/header.py +47 -0
  168. grid_view_spec/types/host.py +47 -0
  169. grid_view_spec/types/json.py +50 -0
  170. grid_view_spec/types/layout.py +68 -0
  171. grid_view_spec/types/lazy.py +55 -0
  172. grid_view_spec/types/media.py +56 -0
  173. grid_view_spec/types/narrowing.py +13 -0
  174. grid_view_spec/types/nav.py +29 -0
  175. grid_view_spec/types/numbers.py +47 -0
  176. grid_view_spec/types/overlay.py +30 -0
  177. grid_view_spec/types/result.py +46 -0
  178. grid_view_spec/types/semantic.py +43 -0
  179. grid_view_spec/types/spec.py +32 -0
  180. grid_view_spec/types/spec_meta.py +22 -0
  181. grid_view_spec/types/table_v2.py +219 -0
  182. grid_view_spec/types/toolbar.py +58 -0
  183. grid_view_spec/types/wire.py +37 -0
  184. grid_view_spec/validate/__init__.py +31 -0
  185. grid_view_spec/validate/chart_options.py +21 -0
  186. grid_view_spec/validate/codes.py +32 -0
  187. grid_view_spec/validate/normalize.py +37 -0
  188. grid_view_spec/validate/refs.py +139 -0
  189. grid_view_spec/validate/validate.py +706 -0
  190. grid_view_spec/validate/wire.py +155 -0
  191. grid_view_spec/wire_decode.py +1054 -0
  192. grid_view_spec-2.0.0.dist-info/METADATA +195 -0
  193. grid_view_spec-2.0.0.dist-info/RECORD +197 -0
  194. grid_view_spec-2.0.0.dist-info/WHEEL +5 -0
  195. grid_view_spec-2.0.0.dist-info/entry_points.txt +2 -0
  196. grid_view_spec-2.0.0.dist-info/licenses/LICENSE +21 -0
  197. 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"