Compare commits

...

65 Commits

Author SHA1 Message Date
Guillaume Baffoin 8442a95de0 debian: update change log 1.6.2 2022-10-07 16:43:22 +02:00
Guillaume Baffoin 5fcb31c19c rules : delete build with pyhton2 2022-10-07 16:19:51 +02:00
Guillaume Baffoin d17db79653 rules : delete build with pyhton2 2022-10-07 16:16:46 +02:00
Guillaume Baffoin b279225b39 debian: remove package python-django-watson 2022-10-07 15:50:33 +02:00
Guillaume Baffoin dee98609fe 1.6.2 packaging 2022-09-16 10:02:34 +02:00
Frédéric Péters 1196e6078f debian: add dh-python to build-depends 2022-09-16 09:47:45 +02:00
Frédéric Péters 7e5ae03c0b debian: build a python3 package (#44412) 2022-09-16 09:47:45 +02:00
Emmanuel Cazenave bd274c96ee 1.5.2 packaging 2022-09-16 09:47:45 +02:00
Dave Hall 14be5bc5bb
Merge pull request #298 from timgates42/bugfix_typos
docs: Fix a few typos
2022-09-03 07:12:24 +01:00
Tim Gates 2e54a8c6c5
docs: Fix a few typos
There are small typos in:
- tests/test_watson/tests.py
- watson/search.py

Fixes:
- Should read `request` rather than `requet`.
- Should read `committed` rather than `commited`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-08-31 20:49:34 +10:00
Dave Hall 67a84c716d
Merge pull request #296 from danihodovic/fix-typo-buildwatson
Fix typo in buildwatson
2022-08-21 13:06:17 +01:00
Dani Hodovic a7ab564e4e
Fix typo in buildwatson 2022-08-19 15:42:07 +02:00
Dave Hall aaee81c9d4 v1.6.2 2022-02-20 16:58:22 +00:00
Dave Hall c05267cca6
Merge pull request #294 from Shybert/django-4-compat
`url` -> `re_path`
2022-02-20 16:56:29 +00:00
Shybert e1aa648a63 `url` -> `re_path`
Required for Django 4 compat
2022-02-13 15:35:26 +01:00
Dave Hall 8d181eb3e0 v1.6.1 2022-01-30 12:47:50 +00:00
Dave Hall 7a4771240e Updated changelog 2022-01-30 12:47:09 +00:00
Dave Hall b3c9ce5931
Merge pull request #293 from CristopherH95/master
Add handling for RelatedField pk types
2022-01-30 12:36:28 +00:00
Cristopher Hernandez 1257eeaed3 Drop Django 1.11 in matrix 2022-01-22 23:07:10 -08:00
Cristopher Hernandez f5026f6e07 Add handling for RelatedField pk types 2022-01-07 19:20:03 -08:00
Dave Hall 077c336dd4 v1.6.0 2021-11-03 15:17:12 +00:00
Dave Hall afec6332d0 Updated changelog 2021-11-03 15:16:52 +00:00
Dave Hall e6af0d3fdc Added new python publish workflow 2021-11-03 15:13:27 +00:00
Dave Hall 0e9b33fed6
Merge pull request #289 from amureki/remove_travis_traces
Remove last traces of Travis CI
2021-10-28 12:54:33 +01:00
Rustem Saiargaliev 95dab01011 Remove last traces of Travis CI 2021-10-28 14:48:58 +03:00
Dave Hall e75f110acd
Merge pull request #288 from CristopherH95/master
Remove deleted objects when rebuilding index
2021-10-26 10:20:08 +01:00
Cristopher Hernandez 4277f7333c Fix database casting issue 2021-10-25 18:29:01 -07:00
Cristopher Hernandez afc990e6e5 Fix type casting issue in deleted entries query 2021-10-24 18:08:05 -07:00
Cristopher Hernandez eb128ceb84 Cast object id lookup to pk field type 2021-10-24 16:54:29 -07:00
Cristopher Hernandez a4636dd38c Remove unused import 2021-10-24 16:32:58 -07:00
Cristopher Hernandez 3177efd4d6 Correct comment in search.py 2021-10-24 16:29:17 -07:00
Cristopher Hernandez 1476d123ba Tweak comments 2021-10-24 16:25:27 -07:00
Cristopher Hernandez 3a69bea8f3 Add cleanup for deleted object entries in buildwatson 2021-10-24 16:24:22 -07:00
Dave Hall 6e94b3879b
Merge pull request #287 from CristopherH95/master
Add AppConfig with default_auto_field set
2021-10-17 12:30:28 +01:00
Cristopher Hernandez 4d47aef2b9 Add AppConfig with default_auto_field set 2021-10-16 11:19:35 -07:00
Dave Hall 9e874d0b25
Merge pull request #283 from Hopiu/master
Added possibility to run tests through Github Actions.
2021-07-26 10:08:19 +01:00
Benedikt Willi aca2511da5 Added possibility to run tests through Github Actions. 2021-07-22 12:05:32 +02:00
Dave Hall 5bc4ff465b v1.5.5 2020-03-30 10:08:58 +01:00
Dave Hall b5f468c12b
Merge pull request #270 from henrikhorluck/fix/django3.0-force_text_deprecation
Change deprecated force_text -> force_str
2020-03-30 10:01:34 +01:00
Henrik Hørlück Berg 24c69ff57c
Change deprecated force_text -> force_str 2020-03-29 12:17:35 +02:00
Dave Hall 63b1986ea6
Merge pull request #267 from timgates42/bugfix_typo_this
docs: Fix simple typo, thie -> this
2020-03-09 20:35:04 +00:00
Tim Gates e87324b466
docs: Fix simple typo, thie -> this
There is a small typo in watson/search.py.

Should read `this` rather than `thie`.
2020-03-10 05:53:33 +11:00
Dave Hall 195d706718 v1.5.4 2020-02-05 10:15:20 +00:00
Dave Hall dbce0a66d7 Updated changelog 2020-02-05 10:15:01 +00:00
Dave Hall 93ac21c16b
Merge pull request #266 from biozz/master
Django 3 support
2020-02-04 10:40:09 +00:00
biozz a2e0844664 bump python and django versions 2020-02-03 20:25:38 +03:00
Ivan Elfimov fafda66869 remove unused variables from travis config 2020-02-03 15:39:52 +03:00
Ivan Elfimov 918590bd06 remove python_2_unicode_compatible from tests 2020-01-31 16:49:56 +03:00
Ivan Elfimov 35796c3154 change test matrix according to `etianen/django-reversion` 2020-01-31 16:41:04 +03:00
Ivan Elfimov 37298a8cdd Add django 3 support 2020-01-31 16:17:54 +03:00
Dave Hall 9be0fd13df Trying to fix pypi deploy 2019-11-01 12:51:56 +00:00
Dave Hall f02ce7521e Trying to fix pypi deploy 2019-11-01 12:25:16 +00:00
Dave Hall c325327804 Adding some test matrix exclusions 2019-11-01 11:07:14 +00:00
Dave Hall 585dbf7d82 Adding some test matrix exclusions 2019-11-01 10:48:27 +00:00
Dave Hall bf5de0fd01 v1.5.3 2019-11-01 10:32:40 +00:00
Dave Hall 62cddee061 Adding more python versions to test matrix 2019-11-01 10:28:42 +00:00
Dave Hall fc0774ddad
Merge pull request #261 from krukas/master
[BUGFIX] #255 Fixed MySQL tests failing mysql + admin context
2019-11-01 10:26:22 +00:00
Krukas e39479cb78 [TASK] Fix flake8 error that regex is not marked as regex 2019-11-01 09:58:25 +01:00
Krukas fd414aae45 [BUGFIX] #255 Fixed MySQL tests failing mysql + admin context
The admin tests failed because of the following error:

(admin.E404) 'django.contrib.messages.context_processors.messages' must be enabled in DjangoTemplates (TEMPLATES) in order to use the admin application.

This is fixed by adding
'django.contrib.messages.context_processors.messages' tot the context_processors

The MySQL tests failed on the error:

django.db.utils.OperationalError: (1055, "Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test_test_project.watson_searchentry.title' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by")

After some diging this hapens because the qeuryset count uses an annotation/aggrogation.
37f8f29377/django/db/models/sql/query.py (L510)

Based on what django recomends for setting the sql_mode to https://docs.djangoproject.com/en/2.2/ref/databases/#setting-sql-mode.
And the recomandation from
https://code.djangoproject.com/ticket/15940#comment:10 to always explicitly set the sql_mode.

I have updated the test MySQL settings to set the sql_mode to STRICT_TRANS_TABLES
2019-11-01 09:49:49 +01:00
Dave Hall ee652e3b33
Merge pull request #260 from krukas/master
[BUGFIX] Fixed buildwatson error with no admin installed
2019-10-31 09:30:49 +00:00
Krukas ab62ec3266 [BUGFIX] Fixed buildwatson error with no admin installed
Buildwatson gives crashes on following error when admin is not
installed:

LookupError: No installed app with label 'admin'
2019-10-30 17:12:41 +01:00
Dave Hall 047007f455
Merge pull request #252 from moggers87/badge-fix
Point PyPI badge at correct project
2018-09-19 10:45:27 +01:00
Matt Molyneaux cf24929928 Point PyPI badge at correct project 2018-09-18 21:20:53 +01:00
Dave Hall 348703e1da
Merge pull request #251 from moggers87/django-2.1
Test against Django 2.1
2018-09-18 19:54:08 +01:00
Matt Molyneaux ae19c32312 Test against Django 2.1 2018-09-17 20:43:44 +01:00
23 changed files with 331 additions and 110 deletions

95
.github/workflows/django.yml vendored Normal file
View File

@ -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

26
.github/workflows/python-publish.yml vendored Normal file
View File

@ -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/*

View File

@ -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

View File

@ -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).

View File

@ -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

11
debian/changelog vendored Normal file
View File

@ -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

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
10

13
debian/control vendored Normal file
View File

@ -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)

7
debian/rules vendored Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/make -f
export PYBUILD_NAME=django-watson
export PYBUILD_DISABLE=test
%:
dh $@ --with python3 --buildsystem=pybuild

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (quilt)

View File

@ -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",
],
)

View File

@ -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+).

View File

@ -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

View File

@ -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)

View File

@ -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),
]

View File

@ -6,4 +6,4 @@ Developed by Dave Hall.
<http://www.etianen.com/>
"""
__version__ = VERSION = (1, 5, 2)
__version__ = VERSION = (1, 6, 2)

7
watson/apps.py Normal file
View File

@ -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'

View File

@ -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."""

View File

@ -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),
)
)

View File

@ -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."""

View File

@ -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)))

View File

@ -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"),
]

View File

@ -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