mirror of https://github.com/django/django.git
Fixed #27143 -- Allowed combining SearchQuery with more than one & or | operators.
This commit is contained in:
parent
40d5011471
commit
978a00e39f
|
@ -92,14 +92,43 @@ class CombinedSearchVector(SearchVectorCombinable, CombinedExpression):
|
|||
super(CombinedSearchVector, self).__init__(lhs, connector, rhs, output_field)
|
||||
|
||||
|
||||
class SearchQuery(Value):
|
||||
class SearchQueryCombinable(object):
|
||||
BITAND = '&&'
|
||||
BITOR = '||'
|
||||
|
||||
def _combine(self, other, connector, reversed, node=None):
|
||||
if not isinstance(other, SearchQueryCombinable):
|
||||
raise TypeError(
|
||||
'SearchQuery can only be combined with other SearchQuerys, '
|
||||
'got {}.'.format(type(other))
|
||||
)
|
||||
if not self.config == other.config:
|
||||
raise TypeError("SearchQuery configs don't match.")
|
||||
if reversed:
|
||||
return CombinedSearchQuery(other, connector, self, self.config)
|
||||
return CombinedSearchQuery(self, connector, other, self.config)
|
||||
|
||||
# On Combinable, these are not implemented to reduce confusion with Q. In
|
||||
# this case we are actually (ab)using them to do logical combination so
|
||||
# it's consistent with other usage in Django.
|
||||
def __or__(self, other):
|
||||
return self._combine(other, self.BITOR, False)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self._combine(other, self.BITOR, True)
|
||||
|
||||
def __and__(self, other):
|
||||
return self._combine(other, self.BITAND, False)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self._combine(other, self.BITAND, True)
|
||||
|
||||
|
||||
class SearchQuery(SearchQueryCombinable, Value):
|
||||
invert = False
|
||||
_output_field = SearchQueryField()
|
||||
config = None
|
||||
|
||||
BITAND = '&&'
|
||||
BITOR = '||'
|
||||
|
||||
def __init__(self, value, output_field=None, **extra):
|
||||
self.config = extra.pop('config', self.config)
|
||||
self.invert = extra.pop('invert', self.invert)
|
||||
|
@ -131,21 +160,6 @@ class SearchQuery(Value):
|
|||
combined.output_field = SearchQueryField()
|
||||
return combined
|
||||
|
||||
# On Combinable, these are not implemented to reduce confusion with Q. In
|
||||
# this case we are actually (ab)using them to do logical combination so
|
||||
# it's consistent with other usage in Django.
|
||||
def __or__(self, other):
|
||||
return self._combine(other, self.BITOR, False)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self._combine(other, self.BITOR, True)
|
||||
|
||||
def __and__(self, other):
|
||||
return self._combine(other, self.BITAND, False)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self._combine(other, self.BITAND, True)
|
||||
|
||||
def __invert__(self):
|
||||
extra = {
|
||||
'invert': not self.invert,
|
||||
|
@ -154,6 +168,12 @@ class SearchQuery(Value):
|
|||
return type(self)(self.value, **extra)
|
||||
|
||||
|
||||
class CombinedSearchQuery(SearchQueryCombinable, CombinedExpression):
|
||||
def __init__(self, lhs, connector, rhs, config, output_field=None):
|
||||
self.config = config
|
||||
super(CombinedSearchQuery, self).__init__(lhs, connector, rhs, output_field)
|
||||
|
||||
|
||||
class SearchRank(Func):
|
||||
function = 'ts_rank'
|
||||
_output_field = FloatField()
|
||||
|
|
|
@ -11,3 +11,6 @@ Bugfixes
|
|||
|
||||
* Fixed a crash in MySQL database validation where ``SELECT @@sql_mode``
|
||||
doesn't return a result (:ticket:`27180`).
|
||||
|
||||
* Allowed combining ``contrib.postgres.search.SearchQuery`` with more than one
|
||||
``&`` or ``|`` operators (:ticket:`27143`).
|
||||
|
|
|
@ -205,14 +205,46 @@ class TestCombinations(GrailTestData, PostgreSQLTestCase):
|
|||
).filter(search=SearchQuery('bedemir') & SearchQuery('scales'))
|
||||
self.assertSequenceEqual(searched, [self.bedemir0])
|
||||
|
||||
def test_query_multiple_and(self):
|
||||
searched = Line.objects.annotate(
|
||||
search=SearchVector('scene__setting', 'dialogue'),
|
||||
).filter(search=SearchQuery('bedemir') & SearchQuery('scales') & SearchQuery('nostrils'))
|
||||
self.assertSequenceEqual(searched, [])
|
||||
|
||||
searched = Line.objects.annotate(
|
||||
search=SearchVector('scene__setting', 'dialogue'),
|
||||
).filter(search=SearchQuery('shall') & SearchQuery('use') & SearchQuery('larger'))
|
||||
self.assertSequenceEqual(searched, [self.bedemir0])
|
||||
|
||||
def test_query_or(self):
|
||||
searched = Line.objects.filter(dialogue__search=SearchQuery('kneecaps') | SearchQuery('nostrils'))
|
||||
self.assertSequenceEqual(set(searched), {self.verse1, self.verse2})
|
||||
|
||||
def test_query_multiple_or(self):
|
||||
searched = Line.objects.filter(
|
||||
dialogue__search=SearchQuery('kneecaps') | SearchQuery('nostrils') | SearchQuery('Sir Robin')
|
||||
)
|
||||
self.assertSequenceEqual(set(searched), {self.verse1, self.verse2, self.verse0})
|
||||
|
||||
def test_query_invert(self):
|
||||
searched = Line.objects.filter(character=self.minstrel, dialogue__search=~SearchQuery('kneecaps'))
|
||||
self.assertEqual(set(searched), {self.verse0, self.verse2})
|
||||
|
||||
def test_query_config_mismatch(self):
|
||||
with self.assertRaisesMessage(TypeError, "SearchQuery configs don't match."):
|
||||
Line.objects.filter(
|
||||
dialogue__search=SearchQuery('kneecaps', config='german') |
|
||||
SearchQuery('nostrils', config='english')
|
||||
)
|
||||
|
||||
def test_query_combined_mismatch(self):
|
||||
msg = "SearchQuery can only be combined with other SearchQuerys, got"
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
Line.objects.filter(dialogue__search=None | SearchQuery('kneecaps'))
|
||||
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
Line.objects.filter(dialogue__search=None & SearchQuery('kneecaps'))
|
||||
|
||||
|
||||
@modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
|
||||
class TestRankingAndWeights(GrailTestData, PostgreSQLTestCase):
|
||||
|
|
Loading…
Reference in New Issue