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

Commit

Permalink
Backport context node schema from 2.1
Browse files Browse the repository at this point in the history
This is a backwards compatible port of the context node changes
in Avocado 2.1 specifically to:

- Support the `field` key instead of `id` as the field identifier
- Support the `concept` key which is used to scope the `field`
- Support branches without children or one child to act as containers
  • Loading branch information
bruth committed Mar 22, 2013
1 parent 25a5594 commit 8436466
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 23 deletions.
14 changes: 14 additions & 0 deletions avocado/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ def get_heuristic_flags(field):
return {
'enumerable': enumerable,
}


# BACKPORT: 2.1
def parse_field_key(key):
"Returns a field lookup based on a variety of key types."
if isinstance(key, int):
return {'pk': key}
keys = ('app_name', 'model_name', 'field_name')
if isinstance(key, basestring):
toks = key.split('.')
elif isinstance(key, (list, tuple)):
toks = key
offset = len(keys) - len(toks)
return dict(zip(keys[offset:], toks))
61 changes: 47 additions & 14 deletions avocado/query/parsers/datacontext.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import warnings
from modeltree.tree import trees
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from avocado.core import utils

AND = 'AND'
OR = 'OR'
BRANCH_KEYS = ('children', 'type')
CONDITION_KEYS = ('id', 'value')
CONDITION_KEYS = ('operator', 'value')
COMPOSITE_KEYS = ('id', 'composite')
LOGICAL_OPERATORS = ('and', 'or')

Expand All @@ -22,16 +24,14 @@ def is_branch(obj):
if has_keys(obj, keys=BRANCH_KEYS):
if obj['type'] not in LOGICAL_OPERATORS:
raise ValidationError('Invalid branch operator')
length = len(obj['children'])
if length < 2:
raise ValidationError('Branch must contain two or more children')
return True


def is_condition(obj):
"Validates required structure for a condition node"
if has_keys(obj, keys=CONDITION_KEYS):
return True
if 'field' in obj or 'id' in obj:
return True


def is_composite(obj):
Expand Down Expand Up @@ -67,10 +67,17 @@ def apply(self, queryset=None, distinct=True):

class Condition(Node):
"Contains information for a single query condition."
def __init__(self, id, operator, value, **context):
self.id = id
def __init__(self, value, operator, id=None, field=None, concept=None, **context):
if field:
self.field_key = field
else:
self.field_key = id
warnings.warn('The "id" key has been replaced with "field"', DeprecationWarning)

self.concept_key = concept
self.operator = operator
self.value = value

super(Condition, self).__init__(**context)

@property
Expand All @@ -80,11 +87,27 @@ def _meta(self):
value=self.value, tree=self.tree, **self.context)
return self.__meta

@property
def concept(self):
if not hasattr(self, '_concept'):
if self.concept_key:
from avocado.models import DataConcept
self._concept = DataConcept.objects.get(id=self.concept_key)
else:
self._concept = None
return self._concept

@property
def field(self):
if not hasattr(self, '_field'):
from avocado.models import DataField
self._field = DataField.objects.get_by_natural_key(self.id)
# Parse to get into a consistent format
field_key = utils.parse_field_key(self.field_key)

if self.concept:
self._field = self.concept.fields.get(**field_key)
else:
self._field = DataField.objects.get(**field_key)
return self._field

@property
Expand Down Expand Up @@ -185,10 +208,19 @@ def validate(attrs, **context):
raise ValidationError(u'DataContext "{0}" does not exist.'.format(attrs['id']))
validate(cxt.json, **context)
elif is_condition(attrs):
from avocado.models import DataField
from avocado.models import DataField, DataConcept

field_key = attrs.get('field', attrs.get('id'))
# Parse to get into a consistent format
field_key = utils.parse_field_key(field_key)

try:
field = DataField.objects.get_by_natural_key(attrs['id'])
except DataField.DoesNotExist, e:
if 'concept' in attrs:
concept = DataConcept.objects.get(id=attrs['concept'])
field = concept.fields.get(**field_key)
else:
field = DataField.objects.get(**field_key)
except ObjectDoesNotExist, e:
raise ValidationError(e.message)
field.validate(operator=attrs['operator'], value=attrs['value'])
elif is_branch(attrs):
Expand All @@ -208,8 +240,9 @@ def parse(attrs, **context):
cxt = DataContext.objects.get(id=attrs['id'])
return parse(cxt.json, **context)
elif is_condition(attrs):
node = Condition(attrs['id'], attrs.get('operator', None), attrs['value'], **context)
node = Condition(operator=attrs['operator'], value=attrs['value'],
id=attrs.get('id'), field=attrs.get('field'), **context)
else:
node = Branch(attrs['type'], **context)
node = Branch(type=attrs['type'], **context)
node.children = map(lambda x: parse(x, **context), attrs['children'])
return node
75 changes: 66 additions & 9 deletions tests/cases/query/tests/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ def test_valid(self):
'value': 'CEO'
}, tree=Employee), None)

# Single by field
self.assertEqual(parsers.datacontext.validate({
'field': 4,
'operator': 'exact',
'value': 'CEO'
}, tree=Employee), None)

# Branch node
self.assertEqual(parsers.datacontext.validate({
'type': 'and',
Expand All @@ -48,6 +55,22 @@ def test_valid(self):
}]
}, tree=Employee), None)

# No children
self.assertEqual(parsers.datacontext.validate({
'type': 'and',
'children': []
}, tree=Employee), None)

# 1 child
self.assertEqual(parsers.datacontext.validate({
'type': 'and',
'children': [{
'id': 4,
'operator': 'exact',
'value': 'CEO'
}]
}, tree=Employee), None)

def test_invalid(self):
# Non-existent data field
self.assertRaises(ValidationError, parsers.datacontext.validate, {
Expand All @@ -66,15 +89,6 @@ def test_invalid(self):
# Invalid logical operator
self.assertRaises(ValidationError, parsers.datacontext.validate, {'type': 'foo', 'children': []})

# No children
self.assertRaises(ValidationError, parsers.datacontext.validate, {'type': 'and', 'children': []})

# 1 child
self.assertRaises(ValidationError, parsers.datacontext.validate, {
'type': 'and',
'children': [{'id': 4, 'operator': 'exact', 'value': 'CEO'}]
})

# Missing 'value' key in first condition
self.assertRaises(ValidationError, parsers.datacontext.validate, {
'type': 'and',
Expand All @@ -85,6 +99,49 @@ def test_invalid(self):
}]
})

def test_field_for_concept(self):
f = DataField.objects.get(id=4)
c1 = DataConcept()
c2 = DataConcept()
c1.save()
c2.save()
cf = DataConceptField(concept=c1, field=f)
cf.save()

self.assertEqual(parsers.datacontext.validate({
'concept': c1.pk,
'field': 4,
'operator': 'exact',
'value': 'CEO'
}, tree=Employee), None)

# Invalid concept
self.assertRaises(ValidationError, parsers.datacontext.validate, {
'concept': c2.pk,
'field': 4,
'operator': 'exact',
'value': 'CEO'
}, tree=Employee)

def test_parsed_node(self):
node = parsers.datacontext.parse({
'type': 'and',
'children': [],
}, tree=Employee)

self.assertEqual(node.condition, None)

node = parsers.datacontext.parse({
'type': 'and',
'children': [{
'id': 4,
'operator': 'exact',
'value': True
}],
}, tree=Employee)

self.assertEqual(str(node.condition), "(AND: ('title__boss__exact', True))")

def test_apply(self):
node = parsers.datacontext.parse({
'id': 4,
Expand Down

0 comments on commit 8436466

Please sign in to comment.