Compare commits
65 Commits
Author | SHA1 | Date |
---|---|---|
Guillaume Baffoin | 8442a95de0 | |
Guillaume Baffoin | 5fcb31c19c | |
Guillaume Baffoin | d17db79653 | |
Guillaume Baffoin | b279225b39 | |
Guillaume Baffoin | dee98609fe | |
Frédéric Péters | 1196e6078f | |
Frédéric Péters | 7e5ae03c0b | |
Emmanuel Cazenave | bd274c96ee | |
Dave Hall | 14be5bc5bb | |
Tim Gates | 2e54a8c6c5 | |
Dave Hall | 67a84c716d | |
Dani Hodovic | a7ab564e4e | |
Dave Hall | aaee81c9d4 | |
Dave Hall | c05267cca6 | |
Shybert | e1aa648a63 | |
Dave Hall | 8d181eb3e0 | |
Dave Hall | 7a4771240e | |
Dave Hall | b3c9ce5931 | |
Cristopher Hernandez | 1257eeaed3 | |
Cristopher Hernandez | f5026f6e07 | |
Dave Hall | 077c336dd4 | |
Dave Hall | afec6332d0 | |
Dave Hall | e6af0d3fdc | |
Dave Hall | 0e9b33fed6 | |
Rustem Saiargaliev | 95dab01011 | |
Dave Hall | e75f110acd | |
Cristopher Hernandez | 4277f7333c | |
Cristopher Hernandez | afc990e6e5 | |
Cristopher Hernandez | eb128ceb84 | |
Cristopher Hernandez | a4636dd38c | |
Cristopher Hernandez | 3177efd4d6 | |
Cristopher Hernandez | 1476d123ba | |
Cristopher Hernandez | 3a69bea8f3 | |
Dave Hall | 6e94b3879b | |
Cristopher Hernandez | 4d47aef2b9 | |
Dave Hall | 9e874d0b25 | |
Benedikt Willi | aca2511da5 | |
Dave Hall | 5bc4ff465b | |
Dave Hall | b5f468c12b | |
Henrik Hørlück Berg | 24c69ff57c | |
Dave Hall | 63b1986ea6 | |
Tim Gates | e87324b466 | |
Dave Hall | 195d706718 | |
Dave Hall | dbce0a66d7 | |
Dave Hall | 93ac21c16b | |
biozz | a2e0844664 | |
Ivan Elfimov | fafda66869 | |
Ivan Elfimov | 918590bd06 | |
Ivan Elfimov | 35796c3154 | |
Ivan Elfimov | 37298a8cdd | |
Dave Hall | 9be0fd13df | |
Dave Hall | f02ce7521e | |
Dave Hall | c325327804 | |
Dave Hall | 585dbf7d82 | |
Dave Hall | bf5de0fd01 | |
Dave Hall | 62cddee061 | |
Dave Hall | fc0774ddad | |
Krukas | e39479cb78 | |
Krukas | fd414aae45 | |
Dave Hall | ee652e3b33 | |
Krukas | ab62ec3266 | |
Dave Hall | 047007f455 | |
Matt Molyneaux | cf24929928 | |
Dave Hall | 348703e1da | |
Matt Molyneaux | ae19c32312 |
|
@ -0,0 +1,95 @@
|
|||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: mysql
|
||||
MYSQL_DATABASE: test_project
|
||||
ports:
|
||||
- 3306:3306
|
||||
postgres:
|
||||
image: postgres:13
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: test_project
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
|
||||
django-version: [2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0]
|
||||
exclude:
|
||||
# Django 4.0 is compatible with Python 3.8+
|
||||
- python-version: "3.6"
|
||||
django-version: "4.0"
|
||||
- python-version: "3.7"
|
||||
django-version: "4.0"
|
||||
# Python 3.8 is compatible with Django 2.2+
|
||||
- python-version: "3.8"
|
||||
django-version: "2.0"
|
||||
- python-version: "3.8"
|
||||
django-version: "2.1"
|
||||
# Python 3.9 is compatible with Django 3.1+
|
||||
- python-version: "3.9"
|
||||
django-version: "2.0"
|
||||
- python-version: "3.9"
|
||||
django-version: "2.1"
|
||||
- python-version: "3.9"
|
||||
django-version: "2.2"
|
||||
- python-version: "3.9"
|
||||
django-version: "3.0"
|
||||
# Python 3.10 is compatible with Django 3.2+
|
||||
- python-version: "3.10"
|
||||
django-version: "2.0"
|
||||
- python-version: "3.10"
|
||||
django-version: "2.1"
|
||||
- python-version: "3.10"
|
||||
django-version: "2.2"
|
||||
- python-version: "3.10"
|
||||
django-version: "3.0"
|
||||
- python-version: "3.10"
|
||||
django-version: "3.1"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: psycopg2 prerequisites
|
||||
run: sudo apt-get install libpq-dev
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 coverage "Django~=${{ matrix.django-version }}.0" "psycopg2==2.8.6" mysqlclient -e .
|
||||
- name: Run Flake8
|
||||
run: |
|
||||
flake8
|
||||
- name: Run Tests
|
||||
run: |
|
||||
coverage run -a tests/runtests.py
|
||||
- name: Run Tests psql
|
||||
run: |
|
||||
coverage run -a tests/runtests.py -d psql
|
||||
env:
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
- name: Run Tests mysql
|
||||
run: |
|
||||
coverage run -a tests/runtests.py -d mysql
|
||||
env:
|
||||
DB_USER: root
|
||||
DB_PASSWORD: mysql
|
|
@ -0,0 +1,26 @@
|
|||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
52
.travis.yml
52
.travis.yml
|
@ -1,52 +0,0 @@
|
|||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
- 3.5
|
||||
- 2.7
|
||||
cache: pip
|
||||
env:
|
||||
global:
|
||||
- PYTHONWARNINGS=default,ignore::PendingDeprecationWarning,ignore::ResourceWarning
|
||||
matrix:
|
||||
- DJANGO='>=2.0,<2.1'
|
||||
- DJANGO='>=1.11,<1.12'
|
||||
- DJANGO='>=1.10,<1.11'
|
||||
- DJANGO='>=1.9,<1.10'
|
||||
- DJANGO='>=1.8,<1.9'
|
||||
matrix:
|
||||
fast_finish: true
|
||||
exclude:
|
||||
- python: 2.7
|
||||
env: DJANGO='>=2.0,<2.1'
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libmysqlclient-dev
|
||||
services:
|
||||
- postgresql
|
||||
- mysql
|
||||
install:
|
||||
- pip install flake8 coverage django$DJANGO psycopg2 mysqlclient -e .
|
||||
before_script:
|
||||
- mysql -e 'create database test_project'
|
||||
- psql -c 'create database test_project;' -U postgres;
|
||||
script:
|
||||
- flake8
|
||||
- coverage run -a tests/runtests.py
|
||||
- coverage run -a tests/runtests.py -d psql
|
||||
- coverage run -a tests/runtests.py -d mysql
|
||||
after_success:
|
||||
- coverage report
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: etianen
|
||||
password:
|
||||
secure: rzaq3pbJz25SVXyR/fn+gLaYxu0LqCEE+wcesg8pjA8cLLvFdLvXi0ZjmixxVl2u4HlndZrUgwTrRGVIQR1TOL0jGRYOUCJDzAaZqJkg+Qykz89iKSnentyOpNJe6fRAHsqFxBESYZjG8JEZvtRv+JIZLY+QR+KCA1xaeu4Afpw=
|
||||
on:
|
||||
tags: true
|
||||
condition: $DJANGO = '>=2.0,<2.1'
|
||||
python: 3.6
|
||||
repo: etianen/django-watson
|
||||
notifications:
|
||||
email: false
|
|
@ -1,5 +1,40 @@
|
|||
# django-watson changelog
|
||||
|
||||
## 1.6.2 - 20/02/2022
|
||||
|
||||
- Django 4.0 compatibility.
|
||||
|
||||
|
||||
## 1.6.1 - 03/01/2022
|
||||
|
||||
- Fix handling for `RelatedField` pk types (@CristopherH95).
|
||||
|
||||
|
||||
## 1.6.0 - 03/11/2021
|
||||
|
||||
- Remove deleted objects when rebuilding index using `buildwatson` (@CristopherH95).
|
||||
- Added Add AppConfig with default_auto_field set (@CristopherH95).
|
||||
- Migrated to GitHub actions from Travis CI (@Hopiu, @etianen, @amureki).
|
||||
|
||||
|
||||
## 1.5.5 - 30/03/2020
|
||||
|
||||
- Fixed a number of deprecation warnings in Django 3.0. (@henrikhorluck).
|
||||
|
||||
|
||||
## 1.5.4 - 05/02/2020
|
||||
|
||||
- Django 3.0 tests and compatibility (@biozz, @ephes).
|
||||
- Removed Python 2.7 and 3.5 support (@biozz).
|
||||
- Removed Django 1.8, 1.9 and 1.10 support (@biozz).
|
||||
|
||||
|
||||
## 1.5.3 - 01/11/2019
|
||||
|
||||
- Fixed `buildwatson` error when `django.contrib.admin` not installed (@krukas).
|
||||
- Bugfixes (@moggers87, @krukas).
|
||||
|
||||
|
||||
## 1.5.2 - 23/02/2018
|
||||
|
||||
- Django 2.0 compatibility improvements (@zandeez, @etianen).
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
django-watson
|
||||
=============
|
||||
|
||||
[![Build Status](https://travis-ci.org/etianen/django-watson.svg?branch=master)](https://travis-ci.org/etianen/django-watson)
|
||||
[![PyPI](https://img.shields.io/pypi/v/nine.svg)](https://pypi.python.org/pypi/django-watson)
|
||||
[![Django CI](https://github.com/etianen/django-watson/actions/workflows/django.yml/badge.svg)](https://github.com/etianen/django-watson/actions/workflows/django.yml)
|
||||
[![PyPI](https://img.shields.io/pypi/v/django-watson.svg)](https://pypi.python.org/pypi/django-watson)
|
||||
[![GitHub license](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://raw.githubusercontent.com/etianen/django-watson/master/LICENSE)
|
||||
|
||||
**django-watson** is a fast multi-model full-text search plugin for Django.
|
||||
|
@ -17,7 +17,7 @@ Features
|
|||
* Order results by relevance.
|
||||
* No need to install additional third-party modules or services.
|
||||
* Fast and scaleable enough for most use cases.
|
||||
* Supports Django 1.8+, Python 2.7+.
|
||||
* Supports Django 2+, Python 3.6+.
|
||||
|
||||
|
||||
Documentation
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
python-django-watson (1.6.2-0) unstable; urgency=low
|
||||
|
||||
* V1.6.2 Release.
|
||||
|
||||
-- Guillaume Baffoin <gbaffoin@entrouvert.com> Fri, 16 Sep 2022 09:52:00 +0200
|
||||
|
||||
python-django-watson (1.5.2-0) unstable; urgency=low
|
||||
|
||||
* Initial Release.
|
||||
|
||||
-- Emmanuel Cazenave <ecazenave@entrouvert.com> Tue, 12 Jun 2018 10:23:43 +0200
|
|
@ -0,0 +1 @@
|
|||
10
|
|
@ -0,0 +1,13 @@
|
|||
Source: python-django-watson
|
||||
Maintainer: Entr'ouvert <info@entrouvert.com>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: python-setuptools, python-all, debhelper (>= 10), dh-python, python3-setuptools, python3-all, python3-django
|
||||
Standards-Version: 3.9.6
|
||||
|
||||
Package: python3-django-watson
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python3:Depends},
|
||||
python3-django
|
||||
Description: Full-text multi-table search application for Django (Python 3)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
export PYBUILD_NAME=django-watson
|
||||
export PYBUILD_DISABLE=test
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
10
setup.py
10
setup.py
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from distutils.core import setup
|
||||
from setuptools import setup
|
||||
from watson import __version__
|
||||
|
||||
setup(
|
||||
|
@ -11,6 +11,7 @@ setup(
|
|||
author_email="dave@etianen.com",
|
||||
url="http://github.com/etianen/django-watson",
|
||||
zip_safe=False,
|
||||
long_description_content_type="text/markdown",
|
||||
packages=[
|
||||
"watson",
|
||||
"watson.management",
|
||||
|
@ -32,10 +33,11 @@ setup(
|
|||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Framework :: Django",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -5,7 +5,12 @@ from optparse import OptionParser
|
|||
|
||||
AVAILABLE_DATABASES = {
|
||||
'psql': {'ENGINE': 'django.db.backends.postgresql_psycopg2'},
|
||||
'mysql': {'ENGINE': 'django.db.backends.mysql'},
|
||||
'mysql': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'OPTIONS': {
|
||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
|
||||
},
|
||||
},
|
||||
'sqlite': {'ENGINE': 'django.db.backends.sqlite3'},
|
||||
}
|
||||
|
||||
|
@ -53,17 +58,20 @@ def main():
|
|||
# database settings
|
||||
if options.database:
|
||||
database_setting = AVAILABLE_DATABASES[options.database]
|
||||
database_default_host = "127.0.0.1"
|
||||
if options.database == "sqlite":
|
||||
database_default_name = os.path.join(os.path.dirname(__file__), "db.sqlite3")
|
||||
else:
|
||||
database_default_name = "test_project"
|
||||
database_setting.update(dict(
|
||||
NAME=os.environ.get("DB_NAME", database_default_name),
|
||||
HOST=os.environ.get("DB_HOST", database_default_host),
|
||||
USER=os.environ.get("DB_USER", ""),
|
||||
PASSWORD=os.environ.get("DB_PASSWORD", "")))
|
||||
else:
|
||||
database_setting = dict(
|
||||
ENGINE=os.environ.get("DB_ENGINE", 'django.db.backends.sqlite3'),
|
||||
HOST=os.environ.get("DB_HOST", database_default_host),
|
||||
NAME=os.environ.get("DB_NAME", os.path.join(os.path.dirname(__file__), "db.sqlite3")),
|
||||
USER=os.environ.get("DB_USER", ""),
|
||||
PASSWORD=os.environ.get("DB_PASSWORD", ""))
|
||||
|
@ -103,9 +111,13 @@ def main():
|
|||
TEMPLATES=[{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': ['templates'],
|
||||
'OPTIONS': {'context_processors': ['django.contrib.auth.context_processors.auth']},
|
||||
'OPTIONS': {'context_processors': [
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
]},
|
||||
'APP_DIRS': True,
|
||||
}],
|
||||
SECRET_KEY="fake-key"
|
||||
)
|
||||
|
||||
# Run Django setup (1.7+).
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TestModelBase(models.Model):
|
||||
title = models.CharField(
|
||||
max_length=200,
|
||||
|
@ -22,7 +21,7 @@ class TestModelBase(models.Model):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return force_text(self.title)
|
||||
return force_str(self.title)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
|
|
@ -22,8 +22,9 @@ from django.core.management import call_command
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django import template
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import force_str
|
||||
from django.db.models import Case, When, Value, IntegerField
|
||||
from django.db import connection
|
||||
|
||||
from watson import search as watson
|
||||
from watson.models import SearchEntry
|
||||
|
@ -186,6 +187,19 @@ class InternalsTest(SearchTestBase):
|
|||
self.assertEqual(watson.search("fooo1").count(), 1)
|
||||
self.assertEqual(watson.search("fooo2").count(), 1)
|
||||
self.assertEqual(watson.search("fooo3").count(), 1)
|
||||
# Use raw deletion query to remove record directly from the database (i.e. no signals triggered).
|
||||
# This is so that the cleanup functionality of buildwatson can be tested.
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
'DELETE FROM ' + WatsonTestModel1._meta.db_table + ' WHERE ' + WatsonTestModel1._meta.pk.name + ' = %s',
|
||||
[self.test11.id]
|
||||
)
|
||||
# Run the rebuild command again.
|
||||
call_command("buildwatson", verbosity=0)
|
||||
# Test that the deleted object is now gone, but the other objects can still be found.
|
||||
self.assertEqual(watson.search("fooo1").count(), 0)
|
||||
self.assertEqual(watson.search("fooo2").count(), 1)
|
||||
self.assertEqual(watson.search("fooo3").count(), 1)
|
||||
|
||||
def testUpdateSearchIndex(self):
|
||||
# Update a model and make sure that the search results match.
|
||||
|
@ -708,7 +722,7 @@ class SiteSearchTest(SearchTestBase):
|
|||
# Test a search that should find everything.
|
||||
response = self.client.get("/simple/json/?q=title")
|
||||
self.assertEqual(response["Content-Type"], "application/json; charset=utf-8")
|
||||
results = set(result["title"] for result in json.loads(force_text(response.content))["results"])
|
||||
results = set(result["title"] for result in json.loads(force_str(response.content))["results"])
|
||||
self.assertEqual(len(results), 6)
|
||||
self.assertTrue("title model1 instance11" in results)
|
||||
self.assertTrue("title model1 instance12" in results)
|
||||
|
@ -740,7 +754,7 @@ class SiteSearchTest(SearchTestBase):
|
|||
self.assertEqual(ex.args[0], "404.html")
|
||||
else:
|
||||
self.assertEqual(response.status_code, 404)
|
||||
# Test a requet for the last page.
|
||||
# Test a request for the last page.
|
||||
response = self.client.get("/custom/?fooo=title&page=last")
|
||||
self.assertEqual(response.context["paginator"].num_pages, 1)
|
||||
self.assertEqual(response.context["page_obj"].number, 1)
|
||||
|
@ -753,7 +767,7 @@ class SiteSearchTest(SearchTestBase):
|
|||
# Test a search that should find everything.
|
||||
response = self.client.get("/custom/json/?fooo=title&page=last")
|
||||
self.assertEqual(response["Content-Type"], "application/json; charset=utf-8")
|
||||
results = set(result["title"] for result in json.loads(force_text(response.content))["results"])
|
||||
results = set(result["title"] for result in json.loads(force_str(response.content))["results"])
|
||||
self.assertEqual(len(results), 6)
|
||||
self.assertTrue("title model1 instance11" in results)
|
||||
self.assertTrue("title model1 instance12" in results)
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from django.conf.urls import url, include
|
||||
from django.contrib import admin
|
||||
|
||||
from django.urls import include, re_path
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
url("^simple/", include("watson.urls")),
|
||||
re_path("^simple/", include("watson.urls")),
|
||||
|
||||
url("^custom/", include("watson.urls"), kwargs={
|
||||
re_path("^custom/", include("watson.urls"), kwargs={
|
||||
"query_param": "fooo",
|
||||
"empty_query_redirect": "/simple/",
|
||||
"extra_context": {
|
||||
|
@ -16,5 +15,5 @@ urlpatterns = [
|
|||
"paginate_by": 10,
|
||||
}),
|
||||
|
||||
url("^admin/", admin.site.urls),
|
||||
re_path("^admin/", admin.site.urls),
|
||||
]
|
||||
|
|
|
@ -6,4 +6,4 @@ Developed by Dave Hall.
|
|||
<http://www.etianen.com/>
|
||||
"""
|
||||
|
||||
__version__ = VERSION = (1, 5, 2)
|
||||
__version__ = VERSION = (1, 6, 2)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WatsonAppConfig(AppConfig):
|
||||
"""App configuration for watson."""
|
||||
name = 'watson'
|
||||
default_auto_field = 'django.db.models.AutoField'
|
|
@ -10,15 +10,14 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.db import transaction, connections, router
|
||||
from django.db.models import Q, FloatField
|
||||
from django.db.models.expressions import RawSQL, Value
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from watson.models import SearchEntry, has_int_pk
|
||||
|
||||
|
||||
def regex_from_word(word):
|
||||
"""Generates a regext from the given search word."""
|
||||
return "(\s{word})|(^{word})".format(
|
||||
"""Generates a regex from the given search word."""
|
||||
return r"(\s{word})|(^{word})".format(
|
||||
word=re.escape(word),
|
||||
)
|
||||
|
||||
|
@ -36,14 +35,14 @@ def escape_query(text, re_escape_chars):
|
|||
normalizes the query text to a format that can be consumed
|
||||
by the backend database
|
||||
"""
|
||||
text = force_text(text)
|
||||
text = force_str(text)
|
||||
text = RE_SPACE.sub(" ", text) # Standardize spacing.
|
||||
text = re_escape_chars.sub(" ", text) # Replace harmful characters with space.
|
||||
text = text.strip()
|
||||
return text
|
||||
|
||||
|
||||
class SearchBackend(six.with_metaclass(abc.ABCMeta)):
|
||||
class SearchBackend(metaclass=abc.ABCMeta):
|
||||
"""Base class for all search backends."""
|
||||
|
||||
def is_installed(self):
|
||||
|
@ -87,7 +86,7 @@ class SearchBackend(six.with_metaclass(abc.ABCMeta)):
|
|||
return connection.ops.quote_name(column_name)
|
||||
|
||||
|
||||
class RegexSearchMixin(six.with_metaclass(abc.ABCMeta)):
|
||||
class RegexSearchMixin(metaclass=abc.ABCMeta):
|
||||
|
||||
"""Mixin to adding regex search to a search backend."""
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.apps import apps
|
|||
from django.contrib import admin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import activate
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -17,7 +17,8 @@ from watson.models import SearchEntry
|
|||
|
||||
|
||||
# Sets up registration for django-watson's admin integration.
|
||||
admin.autodiscover()
|
||||
if apps.is_installed("django.contrib.admin"):
|
||||
admin.autodiscover()
|
||||
|
||||
|
||||
def get_engine(engine_slug_):
|
||||
|
@ -25,7 +26,7 @@ def get_engine(engine_slug_):
|
|||
try:
|
||||
return [x[1] for x in SearchEngine.get_created_engines() if x[0] == engine_slug_][0]
|
||||
except IndexError:
|
||||
raise CommandError("Search Engine \"%s\" is not registered!" % force_text(engine_slug_))
|
||||
raise CommandError("Search Engine \"%s\" is not registered!" % force_str(engine_slug_))
|
||||
|
||||
|
||||
def rebuild_index_for_model(model_, engine_slug_, verbosity_, slim_=False, batch_size_=100, non_atomic_=False):
|
||||
|
@ -50,24 +51,26 @@ def rebuild_index_for_model(model_, engine_slug_, verbosity_, slim_=False, batch
|
|||
print(
|
||||
"Refreshed search entry for {model} {obj} "
|
||||
"in {engine_slug!r} search engine.".format(
|
||||
model=force_text(model_._meta.verbose_name),
|
||||
obj=force_text(obj),
|
||||
engine_slug=force_text(engine_slug_),
|
||||
model=force_str(model_._meta.verbose_name),
|
||||
obj=force_str(obj),
|
||||
engine_slug=force_str(engine_slug_),
|
||||
)
|
||||
)
|
||||
if verbosity_ == 2:
|
||||
print(
|
||||
"Refreshed {local_refreshed_model_count} {model} search entry(s) "
|
||||
"in {engine_slug!r} search engine.".format(
|
||||
model=force_text(model_._meta.verbose_name),
|
||||
model=force_str(model_._meta.verbose_name),
|
||||
local_refreshed_model_count=local_refreshed_model_count[0],
|
||||
engine_slug=force_text(engine_slug_),
|
||||
engine_slug=force_str(engine_slug_),
|
||||
)
|
||||
)
|
||||
if non_atomic_:
|
||||
search_engine_.cleanup_model_index(model_)
|
||||
_bulk_save_search_entries(iter_search_entries(), batch_size=batch_size_)
|
||||
else:
|
||||
with transaction.atomic():
|
||||
search_engine_.cleanup_model_index(model_)
|
||||
_bulk_save_search_entries(iter_search_entries(), batch_size=batch_size_)
|
||||
return local_refreshed_model_count[0]
|
||||
|
||||
|
@ -96,7 +99,7 @@ class Command(BaseCommand):
|
|||
'--non-atomic',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Commit index entries in batches. WARNING: if buildwatson failse, \
|
||||
help="Commit index entries in batches. WARNING: if buildwatson fails, \
|
||||
the index will be incomplete."
|
||||
)
|
||||
parser.add_argument(
|
||||
|
@ -149,7 +152,7 @@ class Command(BaseCommand):
|
|||
if model is None or not search_engine.is_registered(model):
|
||||
raise CommandError(
|
||||
"Model \"%s\" is not registered with django-watson search engine \"%s\"!"
|
||||
% (force_text(model_name), force_text(engine_slug))
|
||||
% (force_str(model_name), force_str(engine_slug))
|
||||
)
|
||||
models.append(model)
|
||||
|
||||
|
@ -172,7 +175,7 @@ class Command(BaseCommand):
|
|||
engine_slugs = [engine_slug]
|
||||
if verbosity >= 2:
|
||||
# let user know the search engine if they selected one
|
||||
print("Rebuilding models registered with search engine \"%s\"" % force_text(engine_slug))
|
||||
print("Rebuilding models registered with search engine \"%s\"" % force_str(engine_slug))
|
||||
else: # loop through all engines
|
||||
engine_slugs = [x[0] for x in SearchEngine.get_created_engines()]
|
||||
|
||||
|
@ -205,7 +208,7 @@ class Command(BaseCommand):
|
|||
"Deleted {stale_entry_count} stale search entry(s) "
|
||||
"in {engine_slug!r} search engine.".format(
|
||||
stale_entry_count=stale_entry_count,
|
||||
engine_slug=force_text(engine_slug),
|
||||
engine_slug=force_str(engine_slug),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -214,6 +217,6 @@ class Command(BaseCommand):
|
|||
"Refreshed {refreshed_model_count} search entry(s) "
|
||||
"in {engine_slug!r} search engine.".format(
|
||||
refreshed_model_count=refreshed_model_count,
|
||||
engine_slug=force_text(engine_slug),
|
||||
engine_slug=force_str(engine_slug),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -5,8 +5,9 @@ from __future__ import unicode_literals
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import python_2_unicode_compatible, force_text
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
try:
|
||||
|
@ -24,6 +25,30 @@ except AttributeError: # Django < 2.0.
|
|||
pass
|
||||
|
||||
|
||||
def get_pk_output_field(model):
|
||||
"""Gets an instance of the field type for the primary key of the given model, useful for database CAST."""
|
||||
pk = model._meta.pk
|
||||
if isinstance(pk, RelatedField):
|
||||
return get_pk_output_field(pk.remote_field.model)
|
||||
field_cls = type(pk)
|
||||
field_kwargs = {}
|
||||
if isinstance(pk, models.CharField):
|
||||
# Some versions of Django produce invalid SQL for the CAST function (in some databases)
|
||||
# if CharField does not have max_length passed.
|
||||
# Therefore, it is necessary to copy over the max_length of the original field to avoid errors.
|
||||
# See: https://code.djangoproject.com/ticket/28371
|
||||
field_kwargs['max_length'] = pk.max_length
|
||||
elif isinstance(pk, models.AutoField):
|
||||
# Some versions of Django appear to also produce invalid SQL in MySQL
|
||||
# when attempting to CAST with AutoField types.
|
||||
# This covers for that by instead casting to the corresponding integer type.
|
||||
if isinstance(pk, models.BigAutoField):
|
||||
field_cls = models.BigIntegerField
|
||||
else:
|
||||
field_cls = models.IntegerField
|
||||
return field_cls(**field_kwargs)
|
||||
|
||||
|
||||
def has_int_pk(model):
|
||||
"""Tests whether the given model has an integer primary key."""
|
||||
pk = model._meta.pk
|
||||
|
@ -36,13 +61,12 @@ def has_int_pk(model):
|
|||
|
||||
|
||||
def get_str_pk(obj, connection):
|
||||
return obj.pk.hex if isinstance(obj.pk, uuid.UUID) and connection.vendor != "postgresql" else force_text(obj.pk)
|
||||
return obj.pk.hex if isinstance(obj.pk, uuid.UUID) and connection.vendor != "postgresql" else force_str(obj.pk)
|
||||
|
||||
|
||||
META_CACHE_KEY = "_meta_cache"
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class SearchEntry(models.Model):
|
||||
|
||||
"""An entry in the search index."""
|
||||
|
|
|
@ -15,9 +15,10 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
|||
from django.db import models, connections, router
|
||||
from django.db.models import Q
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Cast
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import strip_tags
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
try:
|
||||
|
@ -86,11 +87,11 @@ class SearchAdapter(object):
|
|||
# Look up recursive fields.
|
||||
if len(name_parts) == 2:
|
||||
if isinstance(value, (QuerySet, models.Manager)):
|
||||
return " ".join(force_text(self._resolve_field(obj, name_parts[1])) for obj in value.all())
|
||||
return " ".join(force_str(self._resolve_field(obj, name_parts[1])) for obj in value.all())
|
||||
return self._resolve_field(value, name_parts[1])
|
||||
# Resolve querysets.
|
||||
if isinstance(value, (QuerySet, models.Manager)):
|
||||
value = " ".join(force_text(related) for related in value.all())
|
||||
value = " ".join(force_str(related) for related in value.all())
|
||||
# Resolution complete!
|
||||
return value
|
||||
|
||||
|
@ -107,9 +108,9 @@ class SearchAdapter(object):
|
|||
|
||||
You can access the title of the search entry as `entry.title` in your search results.
|
||||
|
||||
The default implementation returns `force_text(obj)` truncated to 1000 characters.
|
||||
The default implementation returns `force_str(obj)` truncated to 1000 characters.
|
||||
"""
|
||||
return force_text(obj)[:1000]
|
||||
return force_str(obj)[:1000]
|
||||
|
||||
def get_description(self, obj):
|
||||
"""
|
||||
|
@ -144,7 +145,7 @@ class SearchAdapter(object):
|
|||
field_names = (field_name for field_name in field_names if field_name not in self.exclude)
|
||||
# Create the text.
|
||||
return self.prepare_content(" ".join(
|
||||
force_text(self._resolve_field(obj, field_name))
|
||||
force_str(self._resolve_field(obj, field_name))
|
||||
for field_name in field_names
|
||||
))
|
||||
|
||||
|
@ -239,7 +240,7 @@ class SearchContextManager(local):
|
|||
objects.add((engine, obj))
|
||||
|
||||
def invalidate(self):
|
||||
"""Marks this search context as broken, so should not be commited."""
|
||||
"""Marks this search context as broken, so should not be committed."""
|
||||
self._assert_active()
|
||||
objects, _ = self._stack[-1]
|
||||
self._stack[-1] = (objects, True)
|
||||
|
@ -366,7 +367,7 @@ class SearchEngine(object):
|
|||
engine_slug=engine_slug,
|
||||
)
|
||||
)
|
||||
# Initialize thie engine.
|
||||
# Initialize this engine.
|
||||
self._registered_models = {}
|
||||
self._engine_slug = engine_slug
|
||||
# Store the search context.
|
||||
|
@ -442,6 +443,26 @@ class SearchEngine(object):
|
|||
model=model,
|
||||
))
|
||||
|
||||
def _get_deleted_entries_for_model(self, model):
|
||||
"""Returns a queryset of entries associated with deleted object instances of the given model"""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from watson.models import SearchEntry, has_int_pk, get_pk_output_field
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
object_id_field = 'object_id_int' if has_int_pk(model) else 'object_id'
|
||||
return SearchEntry.objects.annotate(
|
||||
# normalize the object id into a field of the correct type for the original table
|
||||
normalized_pk=Cast(object_id_field, get_pk_output_field(model))
|
||||
).filter(
|
||||
Q(content_type=content_type) &
|
||||
Q(engine_slug=self._engine_slug) &
|
||||
# subquery to get entries which cannot be found in the original table (when negated)
|
||||
~Q(
|
||||
normalized_pk__in=models.Subquery(
|
||||
model.objects.all().values('pk')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def _get_entries_for_obj(self, obj):
|
||||
"""Returns a queryset of entries associate with the given obj."""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -501,6 +522,11 @@ class SearchEngine(object):
|
|||
# Oh no! Somehow we've got duplicated search entries!
|
||||
search_entries.exclude(id=search_entries[0].id).delete()
|
||||
|
||||
def cleanup_model_index(self, model):
|
||||
"""Removes search index entries which map to deleted object instances for the given model"""
|
||||
search_entries = self._get_deleted_entries_for_model(model)
|
||||
search_entries.delete()
|
||||
|
||||
def update_obj_index(self, obj):
|
||||
"""Updates the search index for the given obj."""
|
||||
_bulk_save_search_entries(list(self._update_obj_index_iter(obj)))
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
|
||||
from watson.views import search, search_json
|
||||
|
||||
app_name = 'watson'
|
||||
urlpatterns = [
|
||||
|
||||
url("^$", search, name="search"),
|
||||
url("^json/$", search_json, name="search_json"),
|
||||
re_path("^$", search, name="search"),
|
||||
re_path("^json/$", search_json, name="search_json"),
|
||||
|
||||
]
|
||||
|
|
|
@ -6,7 +6,6 @@ import json
|
|||
|
||||
from django.shortcuts import redirect
|
||||
from django.http import HttpResponse
|
||||
from django.utils import six
|
||||
from django.views import generic
|
||||
from django.views.generic.list import BaseListView
|
||||
|
||||
|
@ -66,7 +65,7 @@ class SearchMixin(object):
|
|||
context = super(SearchMixin, self).get_context_data(**kwargs)
|
||||
context["query"] = self.query
|
||||
# Process extra context.
|
||||
for key, value in six.iteritems(self.get_extra_context()):
|
||||
for key, value in self.get_extra_context().items():
|
||||
if callable(value):
|
||||
value = value()
|
||||
context[key] = value
|
||||
|
|
Loading…
Reference in New Issue