mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 22:40:26 +00:00
Add support for order_by clause to EavQuerySet (#39)
This commit is contained in:
parent
2891175221
commit
92ba7992d7
4 changed files with 171 additions and 32 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
105
tests/queries.py
105
tests/queries.py
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue