Browse files

Initial commit

  • Loading branch information...
0 parents commit a65e3cebbe55efdb949c2f467f9b6322709d4a20 @coleifer committed May 31, 2010
1 AUTHORS
@@ -0,0 +1 @@
+django-generic-aggregation created by Charles Leifer in 2010
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010 Charles Leifer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
48 README.rst
@@ -0,0 +1,48 @@
+==========================
+django-generic-aggregation
+==========================
+
+annotate() and aggregate() for generically-related data.
+
+Examples
+--------
+
+You want the most commented on blog entries::
+
+ >>> from django.contrib.comments.models import Comment
+ >>> from django.db.models import Count
+ >>> from blog.models import BlogEntry
+ >>> from generic_aggregation import generic_annotate
+
+ >>> annotated = generic_annotate(BlogEntry.objects.all(), Comment.content_object, 'id', Count)
+
+ >>> for entry in annotated:
+ ... print entry.title, entry.score
+
+ The most popular 5
+ The second best 4
+ Nobody commented 0
+
+
+You want to figure out which items are highest rated::
+
+ from django.db.models import Sum, Avg
+
+ # assume a Food model and a generic Rating model
+ apple = Food.objects.create(name='apple')
+
+ # create some ratings on the food
+ Rating.objects.create(content_object=apple, rating=3)
+ Rating.objects.create(content_object=apple, rating=5)
+ Rating.objects.create(content_object=apple, rating=7)
+
+ >>> aggregate = generic_aggregate(Food.objects.all(), Rating.content_object, 'rating', Sum)
+ >>> print aggregate
+ 15
+
+ >>> aggregate = generic_aggregate(Food.objects.all(), Rating.content_object, 'rating', Avg)
+ >>> print aggregate
+ 4
+
+
+Check the tests - there are more examples there. Tested with postgres & sqlite
1 generic_aggregation/__init__.py
@@ -0,0 +1 @@
+from generic_aggregation.utils import generic_aggregate, generic_annotate
0 generic_aggregation/tests/__init__.py
No changes.
20 generic_aggregation/tests/models.py
@@ -0,0 +1,20 @@
+from django.contrib.contenttypes.generic import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+class Food(models.Model):
+ name = models.CharField(max_length=100)
+
+ def __unicode__(self):
+ return self.name
+
+
+class Rating(models.Model):
+ rating = models.IntegerField()
+ created = models.DateTimeField(auto_now_add=True)
+ object_id = models.IntegerField()
+ content_type = models.ForeignKey(ContentType)
+ content_object = GenericForeignKey(ct_field='content_type', fk_field='object_id')
+
+ def __unicode__(self):
+ return '%s rated %s' % (self.content_object, self.rating)
8 generic_aggregation/tests/settings.py
@@ -0,0 +1,8 @@
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_ENGINE = 'postgresql_psycopg2'
+DATABASE_NAME = 'test_main'
+
+INSTALLED_APPS = [
+ 'django.contrib.contenttypes',
+ 'generic_aggregation.tests',
+]
83 generic_aggregation/tests/tests.py
@@ -0,0 +1,83 @@
+import datetime
+
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.test import TestCase
+
+from generic_aggregation import generic_annotate, generic_aggregate
+from generic_aggregation.tests.models import Food, Rating
+
+class SimpleTest(TestCase):
+ def setUp(self):
+ self.apple = Food.objects.create(name='apple')
+ self.orange = Food.objects.create(name='orange')
+
+ dt = datetime.datetime(2010, 1, 1)
+
+ Rating.objects.create(content_object=self.apple, rating=5)
+ Rating.objects.create(content_object=self.apple, rating=3)
+ Rating.objects.create(content_object=self.apple, rating=1, created=dt)
+ Rating.objects.create(content_object=self.apple, rating=7, created=dt)
+
+ Rating.objects.create(content_object=self.orange, rating=4)
+ Rating.objects.create(content_object=self.orange, rating=3)
+ Rating.objects.create(content_object=self.orange, rating=8, created=dt)
+
+ def test_annotation(self):
+ annotated_qs = generic_annotate(Food.objects.all(), Rating.content_object, 'rating', models.Count)
+ self.assertEqual(annotated_qs.count(), 2)
+
+ food_a, food_b = annotated_qs
+
+ self.assertEqual(food_a.score, 4)
+ self.assertEqual(food_a.name, 'apple')
+
+ self.assertEqual(food_b.score, 3)
+ self.assertEqual(food_b.name, 'orange')
+
+ annotated_qs = generic_annotate(Food.objects.all(), Rating.content_object, 'rating', models.Sum)
+ self.assertEqual(annotated_qs.count(), 2)
+
+ food_a, food_b = annotated_qs
+
+ self.assertEqual(food_a.score, 16)
+ self.assertEqual(food_a.name, 'apple')
+
+ self.assertEqual(food_b.score, 15)
+ self.assertEqual(food_b.name, 'orange')
+
+ annotated_qs = generic_annotate(Food.objects.all(), Rating.content_object, 'rating', models.Avg)
+ self.assertEqual(annotated_qs.count(), 2)
+
+ food_b, food_a = annotated_qs
+
+ self.assertEqual(food_b.score, 5)
+ self.assertEqual(food_b.name, 'orange')
+
+ self.assertEqual(food_a.score, 4)
+ self.assertEqual(food_a.name, 'apple')
+
+ def test_aggregation(self):
+ # number of ratings on any food
+ aggregated = generic_aggregate(Food.objects.all(), Rating.content_object, 'rating', models.Count)
+ self.assertEqual(aggregated, 7)
+
+ # total of ratings out there for all foods
+ aggregated = generic_aggregate(Food.objects.all(), Rating.content_object, 'rating', models.Sum)
+ self.assertEqual(aggregated, 31)
+
+ # (showing the use of filters and inner query)
+
+ aggregated = generic_aggregate(Food.objects.filter(name='apple'), Rating.content_object, 'rating', models.Count)
+ self.assertEqual(aggregated, 4)
+
+ aggregated = generic_aggregate(Food.objects.filter(name='orange'), Rating.content_object, 'rating', models.Count)
+ self.assertEqual(aggregated, 3)
+
+ # avg for apple
+ aggregated = generic_aggregate(Food.objects.filter(name='apple'), Rating.content_object, 'rating', models.Avg)
+ self.assertEqual(aggregated, 4)
+
+ # avg for orange
+ aggregated = generic_aggregate(Food.objects.filter(name='orange'), Rating.content_object, 'rating', models.Avg)
+ self.assertEqual(aggregated, 5)
74 generic_aggregation/utils.py
@@ -0,0 +1,74 @@
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection, models
+
+def generic_annotate(queryset, gfk_field, aggregate_field, aggregator=models.Sum, desc=True):
+ ordering = desc and '-score' or 'score'
+ content_type = ContentType.objects.get_for_model(queryset.model)
+
+ qn = connection.ops.quote_name
+
+ # collect the params we'll be using
+ params = (
+ aggregator.name, # the function that's doing the aggregation
+ qn(aggregate_field), # the field containing the value to aggregate
+ qn(gfk_field.model._meta.db_table), # table holding gfk'd item info
+ qn(gfk_field.ct_field + '_id'), # the content_type field on the GFK
+ content_type.pk, # the content_type id we need to match
+ qn(gfk_field.fk_field), # the object_id field on the GFK
+ qn(queryset.model._meta.db_table), # the table and pk from the main
+ qn(queryset.model._meta.pk.name) # part of the query
+ )
+
+ extra = """
+ SELECT %s(%s) AS aggregate_score
+ FROM %s
+ WHERE
+ %s=%s AND
+ %s=%s.%s
+ """ % params
+
+ queryset = queryset.extra(select={
+ 'score': extra
+ },
+ order_by=[ordering])
+
+ return queryset
+
+
+def generic_aggregate(queryset, gfk_field, aggregate_field, aggregator=models.Sum):
+ content_type = ContentType.objects.get_for_model(queryset.model)
+
+ queryset = queryset.values_list('pk') # just the pks
+ inner_query, inner_params = queryset.query.as_nested_sql()
+
+ qn = connection.ops.quote_name
+
+ # collect the params we'll be using
+ params = (
+ aggregator.name, # the function that's doing the aggregation
+ qn(aggregate_field), # the field containing the value to aggregate
+ qn(gfk_field.model._meta.db_table), # table holding gfk'd item info
+ qn(gfk_field.ct_field + '_id'), # the content_type field on the GFK
+ content_type.pk, # the content_type id we need to match
+ qn(gfk_field.fk_field), # the object_id field on the GFK
+ )
+
+ query_start = """
+ SELECT %s(%s) AS aggregate_score
+ FROM %s
+ WHERE
+ %s=%s AND
+ %s IN (
+ """ % params
+
+ query_end = ")"
+
+ # pass in the inner_query unmodified as we will use the cursor to handle
+ # quoting the inner parameters correctly
+ query = query_start + inner_query + query_end
+
+ cursor = connection.cursor()
+ cursor.execute(query, inner_params)
+ row = cursor.fetchone()
+
+ return row[0]

0 comments on commit a65e3ce

Please sign in to comment.