Fixing filter() with a UUID field. Closes #227.

This commit is contained in:
Dave Hall 2017-12-06 16:51:57 +00:00
parent 2f6ccc8bec
commit d6084f7a90
4 changed files with 100 additions and 27 deletions

View File

@ -1,3 +1,4 @@
import uuid
from django.db import models
from django.utils.encoding import force_text, python_2_unicode_compatible
@ -42,8 +43,17 @@ def get_str_pk():
class WatsonTestModel2(TestModelBase):
id = models.CharField(
primary_key=True,
max_length=100,
default=get_str_pk
)
class WatsonTestModel3(TestModelBase):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
)

View File

@ -27,7 +27,7 @@ from django.utils.encoding import force_text
from watson import search as watson
from watson.models import SearchEntry
from test_watson.models import WatsonTestModel1, WatsonTestModel2
from test_watson.models import WatsonTestModel1, WatsonTestModel2, WatsonTestModel3
from test_watson import admin # Force early registration of all admin models. # noQA
@ -67,6 +67,8 @@ class SearchTestBase(TestCase):
model2 = WatsonTestModel2
model3 = WatsonTestModel3
def setUp(self):
# If migrations are off, then this is needed to get the indices installed. It has to
# be called in the setUp() method, but multiple invocations should be safe.
@ -78,6 +80,7 @@ class SearchTestBase(TestCase):
# Register the test models.
watson.register(self.model1)
watson.register(self.model2, exclude=("id",))
watson.register(self.model3, exclude=("id",))
complex_registration_search_engine.register(
WatsonTestModel1, exclude=("content", "description",), store=("is_published",)
)
@ -105,6 +108,16 @@ class SearchTestBase(TestCase):
content="content model2 instance22",
description="description model2 instance22",
)
self.test31 = WatsonTestModel3.objects.create(
title="title model3 instance31",
content="content model3 instance31",
description="description model3 instance31",
)
self.test32 = WatsonTestModel3.objects.create(
title="title model3 instance32",
content="content model3 instance32",
description="description model3 instance32",
)
def tearDown(self):
# Re-register the old registered models.
@ -113,6 +126,7 @@ class SearchTestBase(TestCase):
# Unregister the test models.
watson.unregister(self.model1)
watson.unregister(self.model2)
watson.unregister(self.model3)
complex_registration_search_engine.unregister(WatsonTestModel1)
complex_registration_search_engine.unregister(WatsonTestModel2)
# Delete the test models.
@ -129,37 +143,48 @@ class SearchTestBase(TestCase):
class InternalsTest(SearchTestBase):
def testSearchEntriesCreated(self):
self.assertEqual(SearchEntry.objects.filter(engine_slug="default").count(), 4)
self.assertEqual(SearchEntry.objects.filter(engine_slug="default").count(), 6)
def testBuildWatsonForModelCommand(self):
# Hack a change into the model using a bulk update, which doesn't send signals.
WatsonTestModel1.objects.filter(id=self.test11.id).update(title="fooo1_selective")
WatsonTestModel2.objects.filter(id=self.test21.id).update(title="fooo2_selective")
WatsonTestModel3.objects.filter(id=self.test31.id).update(title="fooo3_selective")
# Test that no update has happened.
self.assertEqual(watson.search("fooo1_selective").count(), 0)
self.assertEqual(watson.search("fooo2_selective").count(), 0)
self.assertEqual(watson.search("fooo3_selective").count(), 0)
# Run the rebuild command.
call_command("buildwatson", "test_watson.WatsonTestModel1", verbosity=0)
# Test that the update is now applied to selected model.
self.assertEqual(watson.search("fooo1_selective").count(), 1)
self.assertEqual(watson.search("fooo2_selective").count(), 0)
call_command("buildwatson", "test_watson.WatsonTestModel1", "test_watson.WatsonTestModel2", verbosity=0)
self.assertEqual(watson.search("fooo3_selective").count(), 0)
call_command(
"buildwatson",
"test_watson.WatsonTestModel1", "test_watson.WatsonTestModel2", "test_watson.WatsonTestModel3",
verbosity=0,
)
# Test that the update is now applied to multiple selected models.
self.assertEqual(watson.search("fooo1_selective").count(), 1)
self.assertEqual(watson.search("fooo2_selective").count(), 1)
self.assertEqual(watson.search("fooo3_selective").count(), 1)
def testBuildWatsonCommand(self):
# Hack a change into the model using a bulk update, which doesn't send signals.
WatsonTestModel1.objects.filter(id=self.test11.id).update(title="fooo1")
WatsonTestModel2.objects.filter(id=self.test21.id).update(title="fooo2")
WatsonTestModel3.objects.filter(id=self.test31.id).update(title="fooo3")
# Test that no update has happened.
self.assertEqual(watson.search("fooo1").count(), 0)
self.assertEqual(watson.search("fooo2").count(), 0)
self.assertEqual(watson.search("fooo3").count(), 0)
# Run the rebuild command.
call_command("buildwatson", verbosity=0)
# Test that the update is now applied.
self.assertEqual(watson.search("fooo1").count(), 1)
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.
@ -226,20 +251,20 @@ class InternalsTest(SearchTestBase):
for search_entry in search_entries.all()[:2]:
search_entry.id = None
search_entry.save()
# Make sure that we have six (including duplicates).
self.assertEqual(search_entries.all().count(), 6)
# Make sure that we have eight (including duplicates).
self.assertEqual(search_entries.all().count(), 8)
# Run the rebuild command.
call_command("buildwatson", verbosity=0)
# Make sure that we have four again (including duplicates).
self.assertEqual(search_entries.all().count(), 4)
# Make sure that we have six again (including duplicates).
self.assertEqual(search_entries.all().count(), 6)
def testEmptyFilterGivesAllResults(self):
for model in (WatsonTestModel1, WatsonTestModel2):
for model in (WatsonTestModel1, WatsonTestModel2, WatsonTestModel3):
self.assertEqual(watson.filter(model, "").count(), 2)
self.assertEqual(watson.filter(model, " ").count(), 2)
def testFilter(self):
for model in (WatsonTestModel1, WatsonTestModel2):
for model in (WatsonTestModel1, WatsonTestModel2, WatsonTestModel3):
# Test can find all.
self.assertEqual(watson.filter(model, "TITLE").count(), 2)
# Test can find a specific one.
@ -268,20 +293,24 @@ class SearchTest(SearchTestBase):
def testMultiTableSearch(self):
# Test a search that should get all models.
self.assertEqual(watson.search("TITLE").count(), 4)
self.assertEqual(watson.search("CONTENT").count(), 4)
self.assertEqual(watson.search("DESCRIPTION").count(), 4)
self.assertEqual(watson.search("TITLE CONTENT DESCRIPTION").count(), 4)
self.assertEqual(watson.search("TITLE").count(), 6)
self.assertEqual(watson.search("CONTENT").count(), 6)
self.assertEqual(watson.search("DESCRIPTION").count(), 6)
self.assertEqual(watson.search("TITLE CONTENT DESCRIPTION").count(), 6)
# Test a search that should get two models.
self.assertEqual(watson.search("MODEL1").count(), 2)
self.assertEqual(watson.search("MODEL2").count(), 2)
self.assertEqual(watson.search("MODEL3").count(), 2)
self.assertEqual(watson.search("TITLE MODEL1").count(), 2)
self.assertEqual(watson.search("TITLE MODEL2").count(), 2)
self.assertEqual(watson.search("TITLE MODEL3").count(), 2)
# Test a search that should get one model.
self.assertEqual(watson.search("INSTANCE11").count(), 1)
self.assertEqual(watson.search("INSTANCE21").count(), 1)
self.assertEqual(watson.search("INSTANCE31").count(), 1)
self.assertEqual(watson.search("TITLE INSTANCE11").count(), 1)
self.assertEqual(watson.search("TITLE INSTANCE21").count(), 1)
self.assertEqual(watson.search("TITLE INSTANCE31").count(), 1)
# Test a search that should get zero models.
self.assertEqual(watson.search("FOOO").count(), 0)
self.assertEqual(watson.search("FOOO INSTANCE11").count(), 0)
@ -352,7 +381,7 @@ class SearchTest(SearchTestBase):
"Search backend does not support prefix matching."
)
def testMultiTablePrefixSearch(self):
self.assertEqual(watson.search("DESCR").count(), 4)
self.assertEqual(watson.search("DESCR").count(), 6)
def testLimitedModelList(self):
# Test a search that should get all models.
@ -362,11 +391,15 @@ class SearchTest(SearchTestBase):
self.assertEqual(watson.search("MODEL1", models=(WatsonTestModel1,)).count(), 2)
self.assertEqual(watson.search("MODEL2", models=(WatsonTestModel1, WatsonTestModel2)).count(), 2)
self.assertEqual(watson.search("MODEL2", models=(WatsonTestModel2,)).count(), 2)
self.assertEqual(watson.search("MODEL3", models=(WatsonTestModel2, WatsonTestModel3)).count(), 2)
self.assertEqual(watson.search("MODEL3", models=(WatsonTestModel3,)).count(), 2)
# Test a search that should get one model.
self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel1, WatsonTestModel2)).count(), 1)
self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel1,)).count(), 1)
self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel1, WatsonTestModel2,)).count(), 1)
self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel2,)).count(), 1)
self.assertEqual(watson.search("INSTANCE31", models=(WatsonTestModel2, WatsonTestModel3,)).count(), 1)
self.assertEqual(watson.search("INSTANCE31", models=(WatsonTestModel3,)).count(), 1)
# Test a search that should get zero models.
self.assertEqual(watson.search("MODEL1", models=(WatsonTestModel2,)).count(), 0)
self.assertEqual(watson.search("MODEL2", models=(WatsonTestModel1,)).count(), 0)
@ -375,17 +408,21 @@ class SearchTest(SearchTestBase):
def testExcludedModelList(self):
# Test a search that should get all models.
self.assertEqual(watson.search("TITLE", exclude=()).count(), 4)
self.assertEqual(watson.search("TITLE", exclude=()).count(), 6)
# Test a search that should get two models.
self.assertEqual(watson.search("MODEL1", exclude=()).count(), 2)
self.assertEqual(watson.search("MODEL1", exclude=(WatsonTestModel2,)).count(), 2)
self.assertEqual(watson.search("MODEL2", exclude=()).count(), 2)
self.assertEqual(watson.search("MODEL2", exclude=(WatsonTestModel1,)).count(), 2)
self.assertEqual(watson.search("MODEL3", exclude=()).count(), 2)
self.assertEqual(watson.search("MODEL3", exclude=(WatsonTestModel1,)).count(), 2)
# Test a search that should get one model.
self.assertEqual(watson.search("INSTANCE11", exclude=()).count(), 1)
self.assertEqual(watson.search("INSTANCE11", exclude=(WatsonTestModel2,)).count(), 1)
self.assertEqual(watson.search("INSTANCE21", exclude=()).count(), 1)
self.assertEqual(watson.search("INSTANCE21", exclude=(WatsonTestModel1,)).count(), 1)
self.assertEqual(watson.search("INSTANCE31", exclude=()).count(), 1)
self.assertEqual(watson.search("INSTANCE31", exclude=(WatsonTestModel1,)).count(), 1)
# Test a search that should get zero models.
self.assertEqual(watson.search("MODEL1", exclude=(WatsonTestModel1,)).count(), 0)
self.assertEqual(watson.search("MODEL2", exclude=(WatsonTestModel2,)).count(), 0)
@ -414,6 +451,10 @@ class SearchTest(SearchTestBase):
title__icontains="MODEL2",
description__icontains="MODEL2",
),)).count(), 2)
self.assertEqual(watson.search("MODEL3", models=(WatsonTestModel3.objects.filter(
title__icontains="MODEL3",
description__icontains="MODEL3",
),)).count(), 2)
# Test a search that should get one model.
self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel1.objects.filter(
title__icontains="MODEL1",
@ -421,6 +462,9 @@ class SearchTest(SearchTestBase):
self.assertEqual(watson.search("INSTANCE21", models=(WatsonTestModel2.objects.filter(
title__icontains="MODEL2",
),)).count(), 1)
self.assertEqual(watson.search("INSTANCE31", models=(WatsonTestModel3.objects.filter(
title__icontains="MODEL3",
),)).count(), 1)
# Test a search that should get no models.
self.assertEqual(watson.search("INSTANCE11", models=(WatsonTestModel1.objects.filter(
title__icontains="MODEL2",
@ -437,7 +481,7 @@ class SearchTest(SearchTestBase):
exclude=(
WatsonTestModel1.objects.filter(title__icontains="FOOO"),
WatsonTestModel2.objects.filter(title__icontains="FOOO"),)
).count(), 4)
).count(), 6)
# Test a search that should get two models.
self.assertEqual(watson.search("MODEL1", exclude=(WatsonTestModel1.objects.filter(
title__icontains="INSTANCE21",
@ -447,6 +491,10 @@ class SearchTest(SearchTestBase):
title__icontains="INSTANCE11",
description__icontains="INSTANCE12",
),)).count(), 2)
self.assertEqual(watson.search("MODEL3", exclude=(WatsonTestModel3.objects.filter(
title__icontains="INSTANCE11",
description__icontains="INSTANCE12",
),)).count(), 2)
# Test a search that should get one model.
self.assertEqual(watson.search("INSTANCE11", exclude=(WatsonTestModel1.objects.filter(
title__icontains="MODEL2",
@ -454,6 +502,9 @@ class SearchTest(SearchTestBase):
self.assertEqual(watson.search("INSTANCE21", exclude=(WatsonTestModel2.objects.filter(
title__icontains="MODEL1",
),)).count(), 1)
self.assertEqual(watson.search("INSTANCE21", exclude=(WatsonTestModel3.objects.filter(
title__icontains="MODEL1",
),)).count(), 1)
# Test a search that should get no models.
self.assertEqual(watson.search("INSTANCE11", exclude=(WatsonTestModel1.objects.filter(
title__icontains="MODEL1",
@ -485,14 +536,16 @@ class LiveFilterSearchTest(SearchTest):
def testUnpublishedModelsNotFound(self):
# Make sure that there are four to find!
self.assertEqual(watson.search("tItle Content Description").count(), 4)
# Unpublish two objects.
self.assertEqual(watson.search("tItle Content Description").count(), 6)
# Unpublish some objects.
self.test11.is_published = False
self.test11.save()
self.test21.is_published = False
self.test21.save()
self.test31.is_published = False
self.test31.save()
# This should return 4, but two of them are unpublished.
self.assertEqual(watson.search("tItle Content Description").count(), 2)
self.assertEqual(watson.search("tItle Content Description").count(), 4)
def testCanOverridePublication(self):
# Unpublish two objects.
@ -635,11 +688,13 @@ class SiteSearchTest(SearchTestBase):
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"])
self.assertEqual(len(results), 4)
self.assertEqual(len(results), 6)
self.assertTrue("title model1 instance11" in results)
self.assertTrue("title model1 instance12" in results)
self.assertTrue("title model2 instance21" in results)
self.assertTrue("title model2 instance22" in results)
self.assertTrue("title model3 instance31" in results)
self.assertTrue("title model3 instance32" in results)
def testSiteSearchCustom(self):
# Test a search than should find everything.
@ -678,11 +733,13 @@ class SiteSearchTest(SearchTestBase):
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"])
self.assertEqual(len(results), 4)
self.assertEqual(len(results), 6)
self.assertTrue("title model1 instance11" in results)
self.assertTrue("title model1 instance12" in results)
self.assertTrue("title model2 instance21" in results)
self.assertTrue("title model2 instance22" in results)
self.assertTrue("title model3 instance31" in results)
self.assertTrue("title model3 instance32" in results)
# Test a search with an invalid page.
response = self.client.get("/custom/json/?fooo=title&page=200")
self.assertEqual(response.status_code, 404)

View File

@ -2,9 +2,11 @@
from __future__ import unicode_literals
import uuid
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import python_2_unicode_compatible
from django.utils.encoding import python_2_unicode_compatible, force_text
from django.utils.functional import cached_property
try:
@ -26,6 +28,10 @@ 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)
META_CACHE_KEY = "_meta_cache"

View File

@ -12,7 +12,7 @@ from weakref import WeakValueDictionary
from django.conf import settings
from django.core.signals import request_finished
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db import models, connections
from django.db import models, connections, router
from django.db.models import Q
from django.db.models.expressions import RawSQL
from django.db.models.query import QuerySet
@ -445,15 +445,15 @@ class SearchEngine(object):
def _get_entries_for_obj(self, obj):
"""Returns a queryset of entries associate with the given obj."""
from django.contrib.contenttypes.models import ContentType
from watson.models import SearchEntry, has_int_pk
from watson.models import SearchEntry, has_int_pk, get_str_pk
model = obj.__class__
content_type = ContentType.objects.get_for_model(model)
object_id = force_text(obj.pk)
# Get the basic list of search entries.
search_entries = SearchEntry.objects.filter(
content_type=content_type,
engine_slug=self._engine_slug,
)
object_id = get_str_pk(obj, connections[search_entries.db])
if has_int_pk(model):
# Do a fast indexed lookup.
object_id_int = int(obj.pk)
@ -471,11 +471,11 @@ class SearchEngine(object):
def _update_obj_index_iter(self, obj):
"""Either updates the given object index, or yields an unsaved search entry."""
from django.contrib.contenttypes.models import ContentType
from watson.models import SearchEntry
from watson.models import SearchEntry, get_str_pk
model = obj.__class__
adapter = self.get_adapter(model)
content_type = ContentType.objects.get_for_model(model)
object_id = force_text(obj.pk)
object_id = get_str_pk(obj, connections[router.db_for_write(ContentType)])
# Create the search entry data.
search_entry_data = {
"engine_slug": self._engine_slug,