Skip to content
This repository has been archived by the owner on Jan 18, 2020. It is now read-only.

Commit

Permalink
Add support for isnull lookups
Browse files Browse the repository at this point in the history
  • Loading branch information
bruth committed Nov 10, 2012
1 parent 5918a3b commit e768608
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 18 deletions.
55 changes: 37 additions & 18 deletions avocado/query/translators.py
Expand Up @@ -13,10 +13,6 @@
INTERNAL_DATATYPE_FORMFIELDS = settings.INTERNAL_DATATYPE_FORMFIELDS


class OperatorNotPermitted(Exception):
pass


class Translator(object):
"""Given a `DataField` instance, a raw value and operator, a
translator validates, cleans and constructs Django compatible
Expand Down Expand Up @@ -59,6 +55,11 @@ def get_operators(self, field):
def _validate_operator(self, field, uid, **kwargs):
# Determine list of allowed operators
allowed_operators = self.get_operators(field)

# Special case for fields that are nullable
if field.field.null:
allowed_operators += ('isnull', '-isnull')

# If uid is None, the default operator will be used
uid = uid or allowed_operators[0]

Expand All @@ -67,11 +68,11 @@ def _validate_operator(self, field, uid, **kwargs):

# No operator is registered
if operator is None:
raise ValueError('"{0}" is not a valid operator'.format(uid))
raise ValidationError('"{0}" is not a valid operator'.format(uid))

# Ensure the operator is allowed
if operator.uid not in allowed_operators:
raise OperatorNotPermitted('Operator "{0}" cannot be used for ' \
raise ValidationError('Operator "{0}" cannot be used for ' \
'this translator'.format(operator))

return operator
Expand All @@ -88,7 +89,7 @@ def _validate_value(self, field, value, **kwargs):
kwargs['form_class'] = get_form_class(name)

# The formfield is being used to clean the value, thus no
# validation errors should be raised.
# 'required' validation errors should be raised.
kwargs['required'] = False

# Special handling for primary keys
Expand Down Expand Up @@ -165,25 +166,37 @@ def _condition(self, field, operator, value, tree):
# Assuming the operator and value validate, check for a NoneType value
# if the operator is 'in'. This condition will be broken out into a
# separate Q object
if operator.lookup == 'in':
if None in value:
add_null = True
value.remove(None)
elif operator.lookup == 'exact' and value is None:
if operator.lookup == 'exact' and value is None:
add_null = True
elif operator.lookup == 'isnull':
add_null = True
else:
# Remove the None value from the list to process separately
if operator.lookup == 'in':
if None in value:
add_null = True
value.remove(None)

# Process a normal value
if value is not None:
condition = tree.query_condition(field, operator.lookup, value)

# Reset value to None for `null` processing
value = None

if value is not None:
condition = tree.query_condition(field, operator.lookup, value)

# The logical OR here is harmless since the only time where a previous
# condition is defined is for an 'in' lookup.
if add_null:
# If value is None, this defaults to isnull=True
if value is None:
value = True
# Read the _get_not_null_pk docs for more info
null_condition = tree.query_condition(field, 'isnull', True) \
& self._get_not_null_pk(field, tree)
null_condition = tree.query_condition(field, 'isnull', value)

if field.model is not tree.root_model:
null_condition = null_condition & \
self._get_not_null_pk(field, tree)

# Tack on to the existing condition if one is defined
if condition is not None:
condition = condition | null_condition
Expand All @@ -192,12 +205,18 @@ def _condition(self, field, operator, value, tree):

if operator.negated:
return ~condition

return condition

def validate(self, field, operator, value, tree, **kwargs):
value = self._get_value(value)
operator = self._validate_operator(field, operator, **kwargs)
value = self._validate_value(field, value, **kwargs)

# This is unique case since the operator is driving the required
# type rather than using the datatype of the field. There is likely
# a more elegant way to do this.
if operator.lookup != 'isnull':
value = self._validate_value(field, value, **kwargs)

if not operator.is_valid(value):
raise ValidationError('"{0}" is not valid for the operator '
Expand Down
18 changes: 18 additions & 0 deletions tests/cases/query/tests/translators.py
@@ -1,5 +1,6 @@
from django.test import TestCase
from django.core import management
from django.core.exceptions import ValidationError
from avocado.models import DataField
from ..models import Employee

Expand All @@ -23,6 +24,10 @@ def test(self):
trans = self.first_name.translate(value='Robert', tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(AND: ('first_name__exact', u'Robert'))")

trans = self.first_name.translate(value=['Robert', None], operator='in',
tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(OR: ('first_name__in', [u'Robert']), ('first_name__isnull', True))")

trans = self.salary.translate(value=None, tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(AND: ('title__salary__isnull', True), ('title__id__isnull', False))")

Expand All @@ -39,3 +44,16 @@ def test_dict(self):

trans = self.salary.translate(value={'value': None, 'label': 'null'}, tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(AND: ('title__salary__isnull', True), ('title__id__isnull', False))")

def test_non_bool_isnull(self):
trans = self.is_manager.translate(value=False, operator='isnull', tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(AND: ('is_manager__isnull', False))")

trans = self.salary.translate(value=False, operator='isnull', tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(AND: ('title__salary__isnull', False), ('title__id__isnull', False))")

self.assertRaises(ValidationError, self.first_name.translate, value=False, operator='isnull', tree=Employee)

trans = self.salary.translate(value=False, operator='isnull', tree=Employee)
self.assertEqual(str(trans['query_modifiers']['condition']), "(AND: ('title__salary__isnull', False), ('title__id__isnull', False))")

0 comments on commit e768608

Please sign in to comment.