django-pitchfork 0.1.0__tar.gz

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 (90) hide show
  1. django_pitchfork-0.1.0/LICENSE +30 -0
  2. django_pitchfork-0.1.0/PKG-INFO +153 -0
  3. django_pitchfork-0.1.0/README.md +118 -0
  4. django_pitchfork-0.1.0/django_pitchfork/__init__.py +47 -0
  5. django_pitchfork-0.1.0/django_pitchfork/admin.py +167 -0
  6. django_pitchfork-0.1.0/django_pitchfork/apps.py +38 -0
  7. django_pitchfork-0.1.0/django_pitchfork/backends/__init__.py +17 -0
  8. django_pitchfork-0.1.0/django_pitchfork/backends/base.py +15 -0
  9. django_pitchfork-0.1.0/django_pitchfork/backends/caps.py +402 -0
  10. django_pitchfork-0.1.0/django_pitchfork/backends/elasticsearch.py +1733 -0
  11. django_pitchfork-0.1.0/django_pitchfork/backends/pitchfork.py +44 -0
  12. django_pitchfork-0.1.0/django_pitchfork/backends/results.py +12 -0
  13. django_pitchfork-0.1.0/django_pitchfork/backends/simple.py +14 -0
  14. django_pitchfork-0.1.0/django_pitchfork/backends/solr.py +1007 -0
  15. django_pitchfork-0.1.0/django_pitchfork/backends/whoosh.py +1144 -0
  16. django_pitchfork-0.1.0/django_pitchfork/backends/xapian.py +1719 -0
  17. django_pitchfork-0.1.0/django_pitchfork/compat/__init__.py +9 -0
  18. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/__init__.py +90 -0
  19. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/admin.py +3 -0
  20. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/backends.py +8 -0
  21. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/constants.py +3 -0
  22. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/exceptions.py +3 -0
  23. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/fields.py +3 -0
  24. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/indexes.py +3 -0
  25. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/inputs.py +3 -0
  26. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/manager.py +3 -0
  27. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/models.py +3 -0
  28. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/query.py +3 -0
  29. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/routers.py +3 -0
  30. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/signals.py +3 -0
  31. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/urls.py +3 -0
  32. django_pitchfork-0.1.0/django_pitchfork/compat/haystack/utils.py +4 -0
  33. django_pitchfork-0.1.0/django_pitchfork/constants.py +40 -0
  34. django_pitchfork-0.1.0/django_pitchfork/exceptions.py +18 -0
  35. django_pitchfork-0.1.0/django_pitchfork/fields.py +29 -0
  36. django_pitchfork-0.1.0/django_pitchfork/forms.py +135 -0
  37. django_pitchfork-0.1.0/django_pitchfork/generic_views.py +140 -0
  38. django_pitchfork-0.1.0/django_pitchfork/indexes.py +37 -0
  39. django_pitchfork-0.1.0/django_pitchfork/inputs.py +12 -0
  40. django_pitchfork-0.1.0/django_pitchfork/management/__init__.py +0 -0
  41. django_pitchfork-0.1.0/django_pitchfork/management/commands/__init__.py +0 -0
  42. django_pitchfork-0.1.0/django_pitchfork/management/commands/build_solr_schema.py +212 -0
  43. django_pitchfork-0.1.0/django_pitchfork/management/commands/clear_index.py +67 -0
  44. django_pitchfork-0.1.0/django_pitchfork/management/commands/haystack_info.py +22 -0
  45. django_pitchfork-0.1.0/django_pitchfork/management/commands/rebuild_index.py +65 -0
  46. django_pitchfork-0.1.0/django_pitchfork/management/commands/update_index.py +431 -0
  47. django_pitchfork-0.1.0/django_pitchfork/manager.py +3 -0
  48. django_pitchfork-0.1.0/django_pitchfork/models.py +3 -0
  49. django_pitchfork-0.1.0/django_pitchfork/panels.py +102 -0
  50. django_pitchfork-0.1.0/django_pitchfork/query.py +14 -0
  51. django_pitchfork-0.1.0/django_pitchfork/routers.py +17 -0
  52. django_pitchfork-0.1.0/django_pitchfork/signals.py +3 -0
  53. django_pitchfork-0.1.0/django_pitchfork/templates/panels/haystack.html +33 -0
  54. django_pitchfork-0.1.0/django_pitchfork/templatetags/__init__.py +0 -0
  55. django_pitchfork-0.1.0/django_pitchfork/templatetags/highlight.py +133 -0
  56. django_pitchfork-0.1.0/django_pitchfork/templatetags/more_like_this.py +121 -0
  57. django_pitchfork-0.1.0/django_pitchfork/urls.py +5 -0
  58. django_pitchfork-0.1.0/django_pitchfork/utils/__init__.py +20 -0
  59. django_pitchfork-0.1.0/django_pitchfork/utils/app_loading.py +10 -0
  60. django_pitchfork-0.1.0/django_pitchfork/utils/geo.py +9 -0
  61. django_pitchfork-0.1.0/django_pitchfork/utils/highlighting.py +5 -0
  62. django_pitchfork-0.1.0/django_pitchfork/utils/loading.py +167 -0
  63. django_pitchfork-0.1.0/django_pitchfork/utils/log.py +3 -0
  64. django_pitchfork-0.1.0/django_pitchfork/views.py +252 -0
  65. django_pitchfork-0.1.0/django_pitchfork.egg-info/PKG-INFO +153 -0
  66. django_pitchfork-0.1.0/django_pitchfork.egg-info/SOURCES.txt +88 -0
  67. django_pitchfork-0.1.0/django_pitchfork.egg-info/dependency_links.txt +1 -0
  68. django_pitchfork-0.1.0/django_pitchfork.egg-info/requires.txt +11 -0
  69. django_pitchfork-0.1.0/django_pitchfork.egg-info/top_level.txt +1 -0
  70. django_pitchfork-0.1.0/pyproject.toml +63 -0
  71. django_pitchfork-0.1.0/setup.cfg +4 -0
  72. django_pitchfork-0.1.0/tests/test_altered_internal_names.py +98 -0
  73. django_pitchfork-0.1.0/tests/test_app_loading.py +70 -0
  74. django_pitchfork-0.1.0/tests/test_backends.py +75 -0
  75. django_pitchfork-0.1.0/tests/test_compat_shim.py +148 -0
  76. django_pitchfork-0.1.0/tests/test_discovery.py +60 -0
  77. django_pitchfork-0.1.0/tests/test_django_config_detection.py +15 -0
  78. django_pitchfork-0.1.0/tests/test_fields.py +781 -0
  79. django_pitchfork-0.1.0/tests/test_forms.py +184 -0
  80. django_pitchfork-0.1.0/tests/test_generic_views.py +81 -0
  81. django_pitchfork-0.1.0/tests/test_indexes.py +926 -0
  82. django_pitchfork-0.1.0/tests/test_inputs.py +84 -0
  83. django_pitchfork-0.1.0/tests/test_loading.py +423 -0
  84. django_pitchfork-0.1.0/tests/test_management_commands.py +162 -0
  85. django_pitchfork-0.1.0/tests/test_managers.py +269 -0
  86. django_pitchfork-0.1.0/tests/test_models.py +205 -0
  87. django_pitchfork-0.1.0/tests/test_query.py +1084 -0
  88. django_pitchfork-0.1.0/tests/test_templatetags.py +102 -0
  89. django_pitchfork-0.1.0/tests/test_utils.py +337 -0
  90. django_pitchfork-0.1.0/tests/test_views.py +308 -0
@@ -0,0 +1,30 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, the django-pitchfork authors.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
+ POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-pitchfork
3
+ Version: 0.1.0
4
+ Summary: Rust-powered search for Django: the haystack-compatible search layer on pitchfork-engine (native Tantivy, Whoosh, Solr, Elasticsearch, Xapian).
5
+ License: BSD-3-Clause
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Environment :: Web Environment
8
+ Classifier: Framework :: Django
9
+ Classifier: Framework :: Django :: 4.2
10
+ Classifier: Framework :: Django :: 5.0
11
+ Classifier: Framework :: Django :: 5.1
12
+ Classifier: Framework :: Django :: 6.0
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: django>=4.2
27
+ Requires-Dist: pitchfork-engine>=0.1.0
28
+ Provides-Extra: whoosh
29
+ Requires-Dist: whoosh<3,>=2.7; extra == "whoosh"
30
+ Provides-Extra: solr
31
+ Requires-Dist: pysolr<5,>=3.9; extra == "solr"
32
+ Provides-Extra: elasticsearch
33
+ Requires-Dist: elasticsearch<8,>=7.17; extra == "elasticsearch"
34
+ Dynamic: license-file
35
+
36
+ # django-pitchfork
37
+
38
+ **Full-text search for Django, powered by Rust.** `django-pitchfork` is a
39
+ drop-in replacement for [django-haystack](https://github.com/django-haystack/django-haystack):
40
+ the same public API your templates, views and management commands already
41
+ speak — `SearchQuerySet`, `SearchIndex`, fields, inputs, views, forms,
42
+ templatetags, management commands — backed by a fast Rust/Tantivy engine
43
+ instead of Python-only indexes.
44
+
45
+ It is the Django-facing search layer on top of
46
+ [pitchfork-engine](https://gitlab.com/co-plus-plus/pitchfork-engine) (the
47
+ search engine itself: a standalone Tantivy-backed HTTP server, a PyO3
48
+ in-process extension, and the haystack backend machinery).
49
+
50
+ ## Why
51
+
52
+ - **Same API, faster core.** Your code keeps using the haystack API; queries
53
+ and indexing run against Rust's Tantivy (GIL released, native query AST).
54
+ - **Escape hatch from unmaintained backends.** Upstream haystack's
55
+ Elasticsearch support tracks ES 7 only; here every backend is tested
56
+ against a live fixture in CI, and gaps are documented, not silent.
57
+ - **One-way dependency.** `django_pitchfork` imports `pitchfork` (the
58
+ engine); the engine never imports the replacement.
59
+
60
+ ## Backends
61
+
62
+ | Backend | Transport | Notes |
63
+ |---|---|---|
64
+ | **pitchfork** (native) | HTTP server or in-process PyO3 | The perf baseline; Rust/Tantivy |
65
+ | Whoosh | file-based index | No service needed |
66
+ | Solr | HTTP (pysolr) | Solr 6 fixture semantics; managed-schema generation |
67
+ | Elasticsearch | HTTP (elasticsearch-py) | 7.x tested; 8.x is a documented gap |
68
+ | Xapian | in-process bindings | Bindings not on PyPI — install from your distro |
69
+
70
+ Each backend ships with a **capability contract** (`caps` module) and a
71
+ generated support matrix ([`docs/backend_support.md`](docs/backend_support.md))
72
+ so documented features are never hand-written claims. Every backend is
73
+ covered by the shared conformance suite (`conformance/run.py`) against live
74
+ fixtures.
75
+
76
+ ## Quick start
77
+
78
+ ```python
79
+ # settings.py
80
+ PITCHFORK_CONNECTIONS = {
81
+ "default": {
82
+ "ENGINE": "django_pitchfork.backends.pitchfork.PitchforkEngine",
83
+ "URL": "http://127.0.0.1:8117/", # the pitchfork-server fixture
84
+ },
85
+ }
86
+ ```
87
+
88
+ ```python
89
+ # myapp/search_indexes.py
90
+ from django_pitchfork import indexes
91
+
92
+ class NoteIndex(indexes.SearchIndex, indexes.Indexable):
93
+ text = indexes.CharField(document=True)
94
+ title = indexes.CharField(model_attr="title")
95
+
96
+ def get_model(self):
97
+ return Note
98
+ ```
99
+
100
+ ```bash
101
+ python manage.py rebuild_index
102
+ python manage.py haystack_info
103
+ ```
104
+
105
+ ```python
106
+ from django_pitchfork.query import SearchQuerySet
107
+
108
+ results = SearchQuerySet().filter(content="haystack").order_by("-pub_date")
109
+ ```
110
+
111
+ Install:
112
+
113
+ ```bash
114
+ python -m pip install "django-pitchfork" # core (engine included)
115
+ python -m pip install "django-pitchfork[whoosh]" # Whoosh
116
+ python -m pip install "django-pitchfork[solr]" # pysolr
117
+ python -m pip install "django-pitchfork[elasticsearch]" # ES 7.x client
118
+ ```
119
+
120
+ Xapian: no extra — the python-xapian bindings are not on PyPI. Install them
121
+ from your distro (`libxapian-dev` + `python3-xapian`) and the backend picks
122
+ them up (or use the xapian-capable image in `docker/xa/`).
123
+
124
+ The engine repo has the full quick start for the server and embedded
125
+ transports: <https://gitlab.com/co-plus-plus/pitchfork-engine/>
126
+
127
+ ## Migrating from django-haystack
128
+
129
+ Mechanical import-sed (`haystack.` → `django_pitchfork.`) is the primary
130
+ path — every public haystack module has a same-named counterpart, and the
131
+ legacy `HAYSTACK_*` setting names keep working. See
132
+ [`MIGRATION.md`](MIGRATION.md).
133
+
134
+ For in-progress migrations an explicit-use compat shim mirrors haystack's
135
+ import surface (parity-tested against a frozen list of haystack's public
136
+ names, with live parity enforced when haystack is installed as a dev dep):
137
+
138
+ ```python
139
+ import django_pitchfork.compat.haystack as haystack
140
+ ```
141
+
142
+ The shim is never installed as `haystack` — that name collides with the
143
+ real distribution.
144
+
145
+ ## Docs & benchmarks
146
+
147
+ - Sphinx docs: `docs/` (`sphinx-build -W -b html docs/ docs/_build/`).
148
+ - Conformance-suite baselines per backend: [`BENCHMARK.md`](BENCHMARK.md).
149
+
150
+ ## License
151
+
152
+ BSD-3-Clause. The haystack API surface it replaces is also BSD-3-Clause
153
+ (django-haystack).
@@ -0,0 +1,118 @@
1
+ # django-pitchfork
2
+
3
+ **Full-text search for Django, powered by Rust.** `django-pitchfork` is a
4
+ drop-in replacement for [django-haystack](https://github.com/django-haystack/django-haystack):
5
+ the same public API your templates, views and management commands already
6
+ speak — `SearchQuerySet`, `SearchIndex`, fields, inputs, views, forms,
7
+ templatetags, management commands — backed by a fast Rust/Tantivy engine
8
+ instead of Python-only indexes.
9
+
10
+ It is the Django-facing search layer on top of
11
+ [pitchfork-engine](https://gitlab.com/co-plus-plus/pitchfork-engine) (the
12
+ search engine itself: a standalone Tantivy-backed HTTP server, a PyO3
13
+ in-process extension, and the haystack backend machinery).
14
+
15
+ ## Why
16
+
17
+ - **Same API, faster core.** Your code keeps using the haystack API; queries
18
+ and indexing run against Rust's Tantivy (GIL released, native query AST).
19
+ - **Escape hatch from unmaintained backends.** Upstream haystack's
20
+ Elasticsearch support tracks ES 7 only; here every backend is tested
21
+ against a live fixture in CI, and gaps are documented, not silent.
22
+ - **One-way dependency.** `django_pitchfork` imports `pitchfork` (the
23
+ engine); the engine never imports the replacement.
24
+
25
+ ## Backends
26
+
27
+ | Backend | Transport | Notes |
28
+ |---|---|---|
29
+ | **pitchfork** (native) | HTTP server or in-process PyO3 | The perf baseline; Rust/Tantivy |
30
+ | Whoosh | file-based index | No service needed |
31
+ | Solr | HTTP (pysolr) | Solr 6 fixture semantics; managed-schema generation |
32
+ | Elasticsearch | HTTP (elasticsearch-py) | 7.x tested; 8.x is a documented gap |
33
+ | Xapian | in-process bindings | Bindings not on PyPI — install from your distro |
34
+
35
+ Each backend ships with a **capability contract** (`caps` module) and a
36
+ generated support matrix ([`docs/backend_support.md`](docs/backend_support.md))
37
+ so documented features are never hand-written claims. Every backend is
38
+ covered by the shared conformance suite (`conformance/run.py`) against live
39
+ fixtures.
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ # settings.py
45
+ PITCHFORK_CONNECTIONS = {
46
+ "default": {
47
+ "ENGINE": "django_pitchfork.backends.pitchfork.PitchforkEngine",
48
+ "URL": "http://127.0.0.1:8117/", # the pitchfork-server fixture
49
+ },
50
+ }
51
+ ```
52
+
53
+ ```python
54
+ # myapp/search_indexes.py
55
+ from django_pitchfork import indexes
56
+
57
+ class NoteIndex(indexes.SearchIndex, indexes.Indexable):
58
+ text = indexes.CharField(document=True)
59
+ title = indexes.CharField(model_attr="title")
60
+
61
+ def get_model(self):
62
+ return Note
63
+ ```
64
+
65
+ ```bash
66
+ python manage.py rebuild_index
67
+ python manage.py haystack_info
68
+ ```
69
+
70
+ ```python
71
+ from django_pitchfork.query import SearchQuerySet
72
+
73
+ results = SearchQuerySet().filter(content="haystack").order_by("-pub_date")
74
+ ```
75
+
76
+ Install:
77
+
78
+ ```bash
79
+ python -m pip install "django-pitchfork" # core (engine included)
80
+ python -m pip install "django-pitchfork[whoosh]" # Whoosh
81
+ python -m pip install "django-pitchfork[solr]" # pysolr
82
+ python -m pip install "django-pitchfork[elasticsearch]" # ES 7.x client
83
+ ```
84
+
85
+ Xapian: no extra — the python-xapian bindings are not on PyPI. Install them
86
+ from your distro (`libxapian-dev` + `python3-xapian`) and the backend picks
87
+ them up (or use the xapian-capable image in `docker/xa/`).
88
+
89
+ The engine repo has the full quick start for the server and embedded
90
+ transports: <https://gitlab.com/co-plus-plus/pitchfork-engine/>
91
+
92
+ ## Migrating from django-haystack
93
+
94
+ Mechanical import-sed (`haystack.` → `django_pitchfork.`) is the primary
95
+ path — every public haystack module has a same-named counterpart, and the
96
+ legacy `HAYSTACK_*` setting names keep working. See
97
+ [`MIGRATION.md`](MIGRATION.md).
98
+
99
+ For in-progress migrations an explicit-use compat shim mirrors haystack's
100
+ import surface (parity-tested against a frozen list of haystack's public
101
+ names, with live parity enforced when haystack is installed as a dev dep):
102
+
103
+ ```python
104
+ import django_pitchfork.compat.haystack as haystack
105
+ ```
106
+
107
+ The shim is never installed as `haystack` — that name collides with the
108
+ real distribution.
109
+
110
+ ## Docs & benchmarks
111
+
112
+ - Sphinx docs: `docs/` (`sphinx-build -W -b html docs/ docs/_build/`).
113
+ - Conformance-suite baselines per backend: [`BENCHMARK.md`](BENCHMARK.md).
114
+
115
+ ## License
116
+
117
+ BSD-3-Clause. The haystack API surface it replaces is also BSD-3-Clause
118
+ (django-haystack).
@@ -0,0 +1,47 @@
1
+ """django_pitchfork — the public haystack replacement layer on pitchfork-engine.
2
+
3
+ One-way import rule: this package imports
4
+ ``pitchfork`` (the engine); the engine never imports this package.
5
+
6
+ The user-facing ``connections`` facade reads ``PITCHFORK_CONNECTIONS`` first
7
+ and falls back to the legacy ``HAYSTACK_CONNECTIONS``, resolving lazily so
8
+ ``import django_pitchfork`` never touches Django settings at import time.
9
+ """
10
+
11
+ from django.conf import settings
12
+ from django.core.exceptions import ImproperlyConfigured
13
+
14
+ from .routers import DefaultRouter # noqa: F401 (public surface)
15
+ from .utils.loading import ConnectionFacade, ConnectionRouter
16
+
17
+ __version__ = "0.0.1"
18
+
19
+ DEFAULT_ALIAS = "default"
20
+
21
+ connections = ConnectionFacade()
22
+ connection_router = ConnectionRouter()
23
+
24
+
25
+ # Legacy-setting guards (haystack parity: fail loudly, don't silently ignore).
26
+ if settings.configured:
27
+ if hasattr(settings, "HAYSTACK_SITECONF"):
28
+ raise ImproperlyConfigured(
29
+ "The HAYSTACK_SITECONF setting is no longer used & can be removed."
30
+ )
31
+ if hasattr(settings, "HAYSTACK_SEARCH_ENGINE"):
32
+ raise ImproperlyConfigured(
33
+ "The HAYSTACK_SEARCH_ENGINE setting has been replaced with "
34
+ "HAYSTACK_CONNECTIONS (or PITCHFORK_CONNECTIONS)."
35
+ )
36
+
37
+
38
+ def reset_search_queries(**kwargs):
39
+ for conn in connections.all():
40
+ if conn:
41
+ conn.reset_queries()
42
+
43
+
44
+ if settings.configured and settings.DEBUG:
45
+ from django.core import signals as django_signals
46
+
47
+ django_signals.request_started.connect(reset_search_queries)
@@ -0,0 +1,167 @@
1
+ from django import VERSION as django_version
2
+ from django.contrib.admin.options import ModelAdmin, csrf_protect_m
3
+ from django.contrib.admin.views.main import SEARCH_VAR, ChangeList
4
+ from django.core.exceptions import PermissionDenied
5
+ from django.core.paginator import InvalidPage, Paginator
6
+ from django.shortcuts import render
7
+ from django.utils.encoding import force_str
8
+ from django.utils.translation import ngettext
9
+
10
+ from django_pitchfork import connections
11
+ from django_pitchfork.constants import DEFAULT_ALIAS
12
+ from django_pitchfork.query import SearchQuerySet
13
+ from django_pitchfork.utils import get_model_ct_tuple
14
+
15
+
16
+ class SearchChangeList(ChangeList):
17
+ def __init__(self, **kwargs):
18
+ self.haystack_connection = kwargs.pop("haystack_connection", DEFAULT_ALIAS)
19
+ super_kwargs = kwargs
20
+ if django_version[0] >= 4:
21
+ super_kwargs["search_help_text"] = "Search..."
22
+ super().__init__(**super_kwargs)
23
+
24
+ def get_results(self, request):
25
+ if SEARCH_VAR not in request.GET:
26
+ return super().get_results(request)
27
+
28
+ # Note that pagination is 0-based, not 1-based.
29
+ sqs = (
30
+ SearchQuerySet(self.haystack_connection)
31
+ .models(self.model)
32
+ .auto_query(request.GET[SEARCH_VAR])
33
+ .load_all()
34
+ )
35
+
36
+ paginator = Paginator(sqs, self.list_per_page)
37
+ # Get the number of objects, with admin filters applied.
38
+ result_count = paginator.count
39
+ full_result_count = (
40
+ SearchQuerySet(self.haystack_connection).models(self.model).all().count()
41
+ )
42
+
43
+ can_show_all = result_count <= self.list_max_show_all
44
+ multi_page = result_count > self.list_per_page
45
+
46
+ # Get the list of objects to display on this page.
47
+ try:
48
+ result_list = paginator.page(self.page_num).object_list
49
+ # Grab just the Django models, since that's what everything else is
50
+ # expecting.
51
+ result_list = [result.object for result in result_list]
52
+ except InvalidPage:
53
+ result_list = ()
54
+
55
+ self.result_count = result_count
56
+ self.full_result_count = full_result_count
57
+ self.result_list = result_list
58
+ self.can_show_all = can_show_all
59
+ self.multi_page = multi_page
60
+ self.paginator = paginator
61
+
62
+
63
+ class SearchModelAdminMixin:
64
+ # haystack connection to use for searching
65
+ haystack_connection = DEFAULT_ALIAS
66
+
67
+ @csrf_protect_m
68
+ def changelist_view(self, request, extra_context=None):
69
+ if not self.has_change_permission(request, None):
70
+ raise PermissionDenied
71
+
72
+ if SEARCH_VAR not in request.GET:
73
+ # Do the usual song and dance.
74
+ return super().changelist_view(request, extra_context)
75
+
76
+ # Do a search of just this model and populate a Changelist with the
77
+ # returned bits.
78
+ indexed_models = (
79
+ connections[self.haystack_connection]
80
+ .get_unified_index()
81
+ .get_indexed_models()
82
+ )
83
+
84
+ if self.model not in indexed_models:
85
+ # Oops. That model isn't being indexed. Return the usual
86
+ # behavior instead.
87
+ return super().changelist_view(request, extra_context)
88
+
89
+ # So. Much. Boilerplate.
90
+ # Why copy-paste a few lines when you can copy-paste TONS of lines?
91
+ list_display = list(self.list_display)
92
+
93
+ kwargs = {
94
+ "haystack_connection": self.haystack_connection,
95
+ "request": request,
96
+ "model": self.model,
97
+ "list_display": list_display,
98
+ "list_display_links": self.list_display_links,
99
+ "list_filter": self.list_filter,
100
+ "date_hierarchy": self.date_hierarchy,
101
+ "search_fields": self.search_fields,
102
+ "list_select_related": self.list_select_related,
103
+ "list_per_page": self.list_per_page,
104
+ "list_editable": self.list_editable,
105
+ "list_max_show_all": self.list_max_show_all,
106
+ "model_admin": self,
107
+ }
108
+ if hasattr(self, "get_sortable_by"): # Django 2.1+
109
+ kwargs["sortable_by"] = self.get_sortable_by(request)
110
+ changelist = SearchChangeList(**kwargs)
111
+ changelist.formset = None
112
+ media = self.media
113
+
114
+ # Build the action form and populate it with available actions.
115
+ # Check actions to see if any are available on this changelist
116
+ actions = self.get_actions(request)
117
+ if actions:
118
+ action_form = self.action_form(auto_id=None)
119
+ action_form.fields["action"].choices = self.get_action_choices(request)
120
+ else:
121
+ action_form = None
122
+
123
+ selection_note = ngettext(
124
+ "0 of %(count)d selected",
125
+ "of %(count)d selected",
126
+ len(changelist.result_list),
127
+ )
128
+ selection_note_all = ngettext(
129
+ "%(total_count)s selected",
130
+ "All %(total_count)s selected",
131
+ changelist.result_count,
132
+ )
133
+
134
+ context = {
135
+ "module_name": force_str(self.model._meta.verbose_name_plural),
136
+ "selection_note": selection_note % {"count": len(changelist.result_list)},
137
+ "selection_note_all": selection_note_all
138
+ % {"total_count": changelist.result_count},
139
+ "title": changelist.title,
140
+ "is_popup": changelist.is_popup,
141
+ "cl": changelist,
142
+ "media": media,
143
+ "has_add_permission": self.has_add_permission(request),
144
+ "opts": changelist.opts,
145
+ "app_label": self.model._meta.app_label,
146
+ "action_form": action_form,
147
+ "actions_on_top": self.actions_on_top,
148
+ "actions_on_bottom": self.actions_on_bottom,
149
+ "actions_selection_counter": getattr(self, "actions_selection_counter", 0),
150
+ }
151
+ context.update(extra_context or {})
152
+ request.current_app = self.admin_site.name
153
+ app_name, model_name = get_model_ct_tuple(self.model)
154
+ return render(
155
+ request,
156
+ self.change_list_template
157
+ or [
158
+ "admin/%s/%s/change_list.html" % (app_name, model_name),
159
+ "admin/%s/change_list.html" % app_name,
160
+ "admin/change_list.html",
161
+ ],
162
+ context,
163
+ )
164
+
165
+
166
+ class SearchModelAdmin(SearchModelAdminMixin, ModelAdmin):
167
+ pass
@@ -0,0 +1,38 @@
1
+ """App config for django_pitchfork."""
2
+
3
+ import logging
4
+
5
+ from django.apps import AppConfig
6
+ from django.conf import settings
7
+
8
+ from . import connection_router, connections
9
+
10
+ SIGNAL_PROCESSOR_PATH_DEFAULT = "pitchfork.signals.BaseSignalProcessor"
11
+
12
+
13
+ class PitchforkConfig(AppConfig):
14
+ name = "django_pitchfork"
15
+ verbose_name = "django_pitchfork"
16
+ signal_processor = None
17
+ stream = None
18
+
19
+ def ready(self):
20
+ # Setup default logging (engine-owned facade).
21
+ log = logging.getLogger("pitchfork")
22
+ self.stream = logging.StreamHandler()
23
+ self.stream.setLevel(logging.INFO)
24
+ log.addHandler(self.stream)
25
+
26
+ # Setup the signal processor.
27
+ if not self.signal_processor:
28
+ signal_processor_path = getattr(
29
+ settings,
30
+ "PITCHFORK_SIGNAL_PROCESSOR",
31
+ getattr(settings, "HAYSTACK_SIGNAL_PROCESSOR", SIGNAL_PROCESSOR_PATH_DEFAULT),
32
+ )
33
+ from django_pitchfork.utils.loading import import_class
34
+
35
+ signal_processor_class = import_class(signal_processor_path)
36
+ self.signal_processor = signal_processor_class(
37
+ connections, connection_router
38
+ )
@@ -0,0 +1,17 @@
1
+ """Backends for django_pitchfork. Public ``ENGINE`` paths live under
2
+ ``backends.{pitchfork,simple}.``; the base-class seam is ``backends.base``
3
+ (re-exports of the engine's machinery).
4
+ """
5
+
6
+ from .base import ( # noqa: F401
7
+ VALID_GAPS,
8
+ BaseEngine,
9
+ BaseSearchBackend,
10
+ BaseSearchQuery,
11
+ EmptyResults,
12
+ SPELLING_SUGGESTION_HAS_NOT_RUN,
13
+ SearchBackend,
14
+ SearchNode,
15
+ SQ,
16
+ log_query,
17
+ )
@@ -0,0 +1,15 @@
1
+ """Base-class seam — re-exports the engine's machinery under the replacement
2
+ namespace (the public extension point for third-party backends)."""
3
+
4
+ from pitchfork.backends.machinery import ( # noqa: F401
5
+ VALID_GAPS,
6
+ BaseEngine,
7
+ BaseSearchBackend,
8
+ BaseSearchQuery,
9
+ EmptyResults,
10
+ SPELLING_SUGGESTION_HAS_NOT_RUN,
11
+ SearchBackend,
12
+ SearchNode,
13
+ SQ,
14
+ log_query,
15
+ )