Add support for order_by clause to EavQuerySet (#39)

This commit is contained in:
Siegmeyer 2018-09-21 13:47:03 +02:00 committed by GitHub
parent 2891175221
commit 92ba7992d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 171 additions and 32 deletions

View file

@ -19,12 +19,14 @@ Q-expressions need to be rewritten for two reasons:
2. To ensure that Q-expression tree is compiled to valid SQL.
For details see: :func:`rewrite_q_expr`.
"""
from itertools import count
from functools import wraps
from django.core.exceptions import FieldError, ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.db.models import Case, IntegerField, Q, When
from django.db.models.query import QuerySet
from django.db.utils import NotSupportedError
from .models import Attribute, Value
@ -282,3 +284,91 @@ class EavQuerySet(QuerySet):
the ``Manager`` get method.
"""
return super(EavQuerySet, self).get(*args, **kwargs)
def order_by(self, *fields):
# Django only allows to order querysets by direct fields and
# foreign-key chains. In order to bypass this behaviour and order
# by EAV attributes, it is required to construct custom order-by
# clause manually using Django's conditional expressions.
# This will be slow, of course.
order_clauses = []
query_clause = self
for term in [t.split('__') for t in fields]:
# Continue only for EAV attributes.
if len(term) == 2 and term[0] == 'eav':
# Retrieve Attribute over which the ordering is performed.
try:
attr = Attribute.objects.get(slug=term[1])
except ObjectDoesNotExist:
raise ObjectDoesNotExist(
'Cannot find EAV attribute "{}"'.format(term[1])
)
field_name = 'value_%s' % attr.datatype
pks_values = Value.objects.filter(
# Retrieve pk-values pairs of the related values
# (i.e. values for the specified attribute and
# belonging to entities in the queryset).
attribute__slug=attr.slug,
entity_id__in=self
).order_by(
# Order values by their value-field of
# appriopriate attribute data-type.
field_name
).values_list(
# Retrieve only primary-keys of the entities
# in the current queryset.
'entity_id', field_name
)
# Retrive ordered values from pk-value list.
_, ordered_values = zip(*pks_values)
# Add explicit ordering and turn
# list of pairs into look-up table.
val2ind = dict(zip(ordered_values, count()))
# Finally, zip ordered pks with their grouped orderings.
entities_pk = [(pk, val2ind[val]) for pk, val in pks_values]
# Using ordered primary-keys, construct
# CASE clause of the form:
#
# CASE
# WHEN id = 2 THEN 1
# WHEN id = 5 THEN 2
# WHEN id = 9 THEN 2
# WHEN id = 4 THEN 3
# END
#
when_clauses = [
When(id=pk, then=i)
for pk, i in entities_pk
]
order_clause = Case(
*when_clauses,
output_field=IntegerField()
)
clause_name = '__'.join(term)
# Use when-clause to construct
# custom order-by clause.
query_clause = query_clause.annotate(
**{clause_name: order_clause}
)
order_clauses.append(clause_name)
elif len(term) >= 2 and term[0] == 'eav':
raise NotSupportedError(
'EAV does not support ordering through '
'foreign-key chains'
)
else:
order_clauses.append(term[0])
return QuerySet.order_by(query_clause, *order_clauses)

View file

@ -79,7 +79,7 @@ class Forms(TestCase):
admin.form = BaseDynamicEntityForm
view = admin.change_view(request, str(self.instance.pk))
own_fields = 1
own_fields = 2
adminform = view.context_data['adminform']
self.assertEqual(

View file

@ -4,6 +4,8 @@ from eav.decorators import register_eav
class Patient(models.Model):
name = models.CharField(max_length=12)
example = models.ForeignKey(
'ExampleModel', null=True, blank=True, on_delete=models.PROTECT)
def __str__(self):
return self.name

View file

@ -1,5 +1,6 @@
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Q
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import Q, Model
from django.db.utils import NotSupportedError
from django.test import TestCase
import eav
@ -34,25 +35,7 @@ class Queries(TestCase):
eav.unregister(Encounter)
eav.unregister(Patient)
def test_get_or_create_with_eav(self):
Patient.objects.get_or_create(name='Bob', eav__age=5)
self.assertEqual(Patient.objects.count(), 1)
self.assertEqual(Value.objects.count(), 1)
Patient.objects.get_or_create(name='Bob', eav__age=5)
self.assertEqual(Patient.objects.count(), 1)
self.assertEqual(Value.objects.count(), 1)
Patient.objects.get_or_create(name='Bob', eav__age=6)
self.assertEqual(Patient.objects.count(), 2)
self.assertEqual(Value.objects.count(), 2)
def test_get_with_eav(self):
p1, _ = Patient.objects.get_or_create(name='Bob', eav__age=6)
self.assertEqual(Patient.objects.get(eav__age=6), p1)
Patient.objects.create(name='Fred', eav__age=6)
self.assertRaises(MultipleObjectsReturned, lambda: Patient.objects.get(eav__age=6))
def test_filtering_on_normal_and_eav_fields(self):
def init_data(self):
yes = self.yes
no = self.no
@ -74,12 +57,33 @@ class Queries(TestCase):
eav__country=row[4]
)
def test_get_or_create_with_eav(self):
Patient.objects.get_or_create(name='Bob', eav__age=5)
self.assertEqual(Patient.objects.count(), 1)
self.assertEqual(Value.objects.count(), 1)
Patient.objects.get_or_create(name='Bob', eav__age=5)
self.assertEqual(Patient.objects.count(), 1)
self.assertEqual(Value.objects.count(), 1)
Patient.objects.get_or_create(name='Bob', eav__age=6)
self.assertEqual(Patient.objects.count(), 2)
self.assertEqual(Value.objects.count(), 2)
def test_get_with_eav(self):
p1, _ = Patient.objects.get_or_create(name='Bob', eav__age=6)
self.assertEqual(Patient.objects.get(eav__age=6), p1)
Patient.objects.create(name='Fred', eav__age=6)
self.assertRaises(MultipleObjectsReturned, lambda: Patient.objects.get(eav__age=6))
def test_filtering_on_normal_and_eav_fields(self):
self.init_data()
# Check number of objects in DB.
self.assertEqual(Patient.objects.count(), 5)
self.assertEqual(Value.objects.count(), 20)
# Nobody
q1 = Q(eav__fever=yes) & Q(eav__fever=no)
q1 = Q(eav__fever=self.yes) & Q(eav__fever=self.no)
p = Patient.objects.filter(q1)
self.assertEqual(p.count(), 0)
@ -90,26 +94,26 @@ class Queries(TestCase):
self.assertEqual(p.count(), 2)
# Anne
q1 = Q(eav__city__contains='Y') & Q(eav__fever=no)
q1 = Q(eav__city__contains='Y') & Q(eav__fever=self.no)
q2 = Q(eav__age=3)
p = Patient.objects.filter(q1 & q2)
self.assertEqual(p.count(), 1)
# Anne, Daniel
q1 = Q(eav__city__contains='Y', eav__fever=no)
q1 = Q(eav__city__contains='Y', eav__fever=self.no)
q2 = Q(eav__city='Nice')
q3 = Q(eav__age=3)
p = Patient.objects.filter((q1 | q2) & q3)
self.assertEqual(p.count(), 2)
# Everyone
q1 = Q(eav__fever=no) | Q(eav__fever=yes)
q1 = Q(eav__fever=self.no) | Q(eav__fever=self.yes)
p = Patient.objects.filter(q1)
self.assertEqual(p.count(), 5)
# Anne, Bob, Daniel
q1 = Q(eav__fever=no) # Anne, Bob, Daniel
q2 = Q(eav__fever=yes) # Cyrill, Eugene
q1 = Q(eav__fever=self.no) # Anne, Bob, Daniel
q2 = Q(eav__fever=self.yes) # Cyrill, Eugene
q3 = Q(eav__country__contains='e') # Cyrill, Daniel, Eugene
q4 = q2 & q3 # Cyrill, Daniel, Eugene
q5 = (q1 | q4) & q1 # Anne, Bob, Daniel
@ -123,7 +127,7 @@ class Queries(TestCase):
# Anne, Bob, Daniel
q1 = Q(eav__city__contains='Y')
q2 = Q(eav__fever=no)
q2 = Q(eav__fever=self.no)
q3 = q1 | q2
p = Patient.objects.filter(q3)
self.assertEqual(p.count(), 3)
@ -134,6 +138,49 @@ class Queries(TestCase):
self.assertEqual(p.count(), 2)
# Eugene
q1 = Q(name__contains='E', eav__fever=yes)
q1 = Q(name__contains='E', eav__fever=self.yes)
p = Patient.objects.filter(q1)
self.assertEqual(p.count(), 1)
def test_order_by(self):
def order(ordering):
query = Patient.objects.all().order_by(*ordering)
return list(query.values_list('name', flat=True))
self.init_data()
self.assertEqual(
['Bob', 'Eugene', 'Cyrill', 'Anne', 'Daniel'],
order(['eav__city'])
)
self.assertEqual(
['Eugene', 'Anne', 'Daniel', 'Bob', 'Cyrill'],
order(['eav__age', 'eav__city'])
)
self.assertEqual(
['Eugene', 'Cyrill', 'Anne', 'Daniel', 'Bob'],
order(['eav__fever', 'eav__age'])
)
self.assertEqual(
['Eugene', 'Cyrill', 'Daniel', 'Bob', 'Anne'],
order(['eav__fever', '-name'])
)
self.assertEqual(
['Eugene', 'Daniel', 'Cyrill', 'Bob', 'Anne'],
order(['-name', 'eav__age'])
)
self.assertEqual(
['Anne', 'Bob', 'Cyrill', 'Daniel', 'Eugene'],
order(['example__name'])
)
with self.assertRaises(NotSupportedError):
Patient.objects.all().order_by('eav__first__second')
with self.assertRaises(ObjectDoesNotExist):
Patient.objects.all().order_by('eav__nonsense')