This will get the result in a single SQL query:
# views.py
from django.db.models import Count
from .models import Source
def get_tag_count():
"""
Returns the count of tags associated with each source
"""
sources = Source.objects.annotate(tag_count=Count('quote__tags')) \
.values('title', 'quote__tags__name', 'tag_count') \
.order_by('title')
# Groupe the results as
# {source: {tag: count}}
grouped = {}
for source in sources:
title = source['title']
tag = source['quote__tags__name']
count = source['tag_count']
if not title in grouped:
grouped[title] = {}
grouped[title][tag] = count
return grouped
# in template.html
{% for source, tags in sources.items %}
<h3>{{ source }}</h3>
{% for tag, count in tags.items %}
{% if tag %}
<p>{{ tag }} : {{ count }}</p>
{% endif %}
{% endfor %}
{% endfor %}
Complementary tests :)
# tests.py
from django.test import TestCase
from .models import Source, Tag, Quote
from .views import get_tag_count
class SourceTags(TestCase):
def setUp(self):
abc = Source.objects.create(title='ABC')
xyz = Source.objects.create(title='XYZ')
inspire = Tag.objects.create(name='Inspire', slug='inspire')
lol = Tag.objects.create(name='lol', slug='lol')
q1 = Quote.objects.create(text='I am inspired foo', source=abc)
q2 = Quote.objects.create(text='I am inspired bar', source=abc)
q3 = Quote.objects.create(text='I am lol bar', source=abc)
q1.tags = [inspire]
q2.tags = [inspire]
q3.tags = [inspire, lol]
q1.save(), q2.save(), q3.save()
def test_count(self):
# Ensure that only 1 SQL query is done
with self.assertNumQueries(1):
sources = get_tag_count()
self.assertEqual(sources['ABC']['Inspire'], 3)
self.assertEqual(sources['ABC']['lol'], 1)
I have basically used the annotate
and values
functions from the ORM. They are very powerful because they automatically perform the joins. They are also very efficient because they hit the database only once, and return only those fields which are specified.