Handling related models in Django for use in Django-Piston - django

I have setup like so (changed for simplicity)
class Author(models.Model)
name = models.CharField(max_length=100)
...
class Document(models.Model):
title = models.CharField(max_length=200)
content - models.TextField()
author = models.ForeignKey("Author", related_name="documents")
date_published = models.DateTimeField()
categories = models.ManyToManyField("Category")
class Category(models.Model):
name = models.CharField(max_length=100)
I'm pulling in the Author records but I only want to pull in related document records for each author that match specific criteria -- say, date_published and category.
I know the easy way to do this would be to pull in the records as a list of dictionaries using Author.objects.values(), looping through each record and running:
author['documents']=Document.objects.filter(categories__in=[category_list], date_published__year=year)`
However, this is being generated for django-piston, and it seems to be happiest (particularly if you're defining your own fields!) if you return a QuerySet object.
Part of this may be because I made changes to the base django-piston code. Basically, the current version of the code here overwrites the fields value. I changed this code so that I could change the fields value for a Handler based on the request (so I could provide more details if the request was for a specific resource).
So I guess my question is three-fold:
Is there a way to filter or somehow limit the subrecords of a record (i.e. filter documents for each author.documents)
If not, what is a functional way of doing this that also works with django-piston?
Is there some easier, better way to do what I'm trying to do (display all the authors without their documents if an id is not given, but displaying the sub-records if filtering to just one author)?
Clarification
Okay, just to be clear, here is the pseudocode that I want:
def perhaps_impossible_view(request, categories=None, year=None):
authors = Author.objects.all()
authors.something_magical_happens_to_documents(category__in=[categories], date_published__year=year)
return render_to_response('blar.html', locals(), RequestContext(request))
So that if I were to put it in a template, this would work without any modifications:
{% for a in authors %}
{% for d in authors.documents.all %}
{{ d.title }} is almost certainly in one of these categories: {{ categories }} and was absolutely published in {{ year }}. If not, .something_magical_happens_to_documents didn't work.
{% endfor %}
{% endfor %}
something_magical_happens_to_documents has to run and actually change the contents of documents for each author record. It seems like this should be possible, but perhaps it isn't?

Edited... or better... replaced! :)
That's true the authors without document matching won't be in the queryset so you will have to add them back after (I couldn't find a better way but maybe someone knows how to not remove them without using raw sql).
You get the full documents count of the authors because you don't use the queryset to get the document counts:
qryset = Author.objects.all()
qryset = qryset.filter(documents__categories__name__in = category_list).distinct()
qryset = qryset.filter(documents__date_published__year=year)
print(qryset) gives [<Author: Author object>] (if only 1 author matched all categories) and qryset[0].documents.count() will return only the number of documents matched (not all documents from the author - 2 in the case I tested and the author had 4 but only 2 matching all conditions).
If you use dict (.values()) instead of querysets in the steps above, you can't do that (I think) because dict won't have a documents field so:
qryset_dict = Author.objects.values()
qryset_dict = qryset_dict.filter(documents__categories__name__in = category_list).distinct().values()
qryset_dict = qryset_dict.filter(documents__date_published__year=year).values()
when you issue qryset_dict[0].documents.count() you receive an error:
AttributeError: 'dict' object has no attribute 'documents'
Now to add the filtered authors back you can do:
res = []
for a in Author.objects.all():
if a in qryset:
res.append([a,a.documents.count()])
else:
res.append([a,0])
and res will be a list with <authors> in 1st column and count of documents matching in 2nd column.
I know this is far from perfect... but if you are interested only in the count() of matching documents per author, I think you could find a better way using django aggregation and annotation or possibly make it with raw sql using a left join from authors to documents.
EDIT after Clarification in Question:
def possible_view(request, categories=None, year=None):
# you will pass these as parmeters of course
category_list = ['c2', 'c3']
year = 2010
qryset = Document.objects.filter(categories__name__in = category_list).distinct()
qryset = qryset.filter(date_published__year=year)
authors = Author.objects.all()
return render_to_response('blar.html', { 'result': qryset, 'authors': authors, 'categories': category_list, 'year': year }, RequestContext(request))
Template blar.html:
{% for a in authors %}
<b>{{a.name}}</b><br />
{% for d in result %}
{% if d.author.name == a.name %}
{{ d.title }} is almost certainly in one of these categories: {{ categories }} and was absolutely published in {{ year }}. If not, .something_magical_happens_to_documents didn't work.<br />
{% endif %}
{% endfor %}<br />
{% endfor %}
This will give you something not very pretty but with all authors and below each one, the list of their documents that fall within one of the category_list (OR condition, for AND, you need to filter the query for each category instead of using __in).
If the author has no document in the category_list, it wil be listed without documents below him.
Something like:
aut1
tit2 is almost certainly in one of these categories: ['c2', 'c3'] and was absolutely published in 2010. If not, .something_magical_happens_to_documents didn't work.
tit1 is almost certainly in one of these categories: ['c2', 'c3'] and was absolutely published in 2010. If not, .something_magical_happens_to_documents didn't work.
aut2
tit3 is almost certainly in one of these categories: ['c2', 'c3'] and was absolutely published in 2010. If not, .something_magical_happens_to_documents didn't work.
aut3

Related

Django do something with each different value in a model field

I know I've seen this before, but I can't find now that I am ready to implement it.
I'm trying to list items on a page by category and subcategory, but I only want 1 category and then the subcategories in the category. Hopefully my code will make sense.
class Objects(models.Model):
# Main Checkbox.
category = models.CharField(
max_length=100,
blank=True,
)
# Checkboxes under Main Checkbox.
subcategory = models.CharField(
max_length=100,
blank=True,
)
So my objects are stored as:
category1, subcategory1
category1, subcategory2
category2, subcategory1
category2, subcategory2
And when displayed should give me:
category1
subcategory1
subcategory2
category2
subcategory1
subcategory2
How do I set my query so that my results show me each "different" category?
First off you should look to normalise your objects, meaning you could keep your subcategories as a separate object and use models.foreignKey to link them together. See Willem Van Onsem's answer for this.
However, to solve the current problem you should be able to utilize the regroup template tag in order to aggregate the categories.
In your case it would look something like:
{% regroup objects by category as categories %}
{% for category in categories %}
{{ category.grouper }}
{% for subcategory in category.list %}
{{ subcategory }}
{% endfor %}
{% endfor %}
The data duplication anti-pattern
Many computer scientists see this as bad design, since it introduces data duplication. Imagine that you later want to change the name of a category, then that means you need to find all occurrences of that category, and rename them. If you only use this for the Objects model, then that is perhaps doable, but if all sorts of elements belong to Categorys, then this easily gets out of hand.
Furthermore it also restricts categories: two different categories can never have the same name (which might here be reasonable), nor can we attach much properties to the Category: imagine that we want to add a description for the category, then that description needs to be repeated over all rows, or if we decide to store it only in one row, then it will be hard to find that specific row. Furthermore if there are two rows with different descriptions, then what description to pick?
The database will also be very huge: each row should repeat the same category. If a category on average takes 15 characters, that means that we will - depending on the encoding - easily waste 8 bytes per row (well the row contains 16 bytes for a string given it is UTF-8 encoding, and only ASCII characters, but a ForeignKey will frequently use 8 bytes). If we would add a description that has on average 63 characters, then we would again waste another 64 bytes per row. For a small amount of rows, that is not a problem, but the problem easily scales problematic. The above are of course only estimates on what problems might arise, do not see this as the "real numbers": the size a database takes is determined by a lot of parameters that are here either ignored, or estimated.
Yes all these problems probably can be solved, but instead of solving the problems ad-hoc, it is better to normalize the database.
Normalizing the models
Normalization typically means that we introduce extra tables that store for example one record per Category, and use ForeignKeys to refer to that record. For you example a normalized variant would be:
class Category(models.Model):
name = models.CharField(max_length=100)
class SubCategory(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Object(models.Model):
subcategory = models.ForeignKey(
SubCategory,
null=True,
on_delete=models.SET_NULL
)
So we store Categorys and SubCategorys in dedicated tables, and link the models together with ForeignKeys.
Rendering lists of (Sub)Categorys
Now that we normalized the models, we can effectively render the Categorys with:
# app/views.py
def some_view(request):
categories = Category.objects.prefetch_related('subcategory_set')
return render(request, 'app/some_template.html', {'categories': categories})
and in the app/templates/some_template.html we then can render it as:
<ul>
{% for cat in categories %}
<li>{{ cat.name }}</li>
<ul>
{% for subcat in cat.subcategory_set %}
<li>{{ subcat.name }}</li>
{% endfor %}
</ul>
{% endfor %}
</ul>
We thus iterate over all categories, and for every cat, we iterate over the subcategory_set.

Django template limit output of the for cycle

So I have a template:
{% for Chapter in latest_chapter_list %}
{% ifequal Chapter.manga|truncatechars:20 Manga.name|truncatechars:20 %}
{{Chapter.chapter}}
{% endifequal %}
{% endfor %}
models:
class Manga(models.Model):
name = models.CharField(max_length=20,help_text='Name of the comic/manga')
class Chapter(models.Model):
manga = models.ForeignKey(Manga)
chapter = models.IntegerField(default=1, help_text='Number of a chapter')
So what I want is that the template would only display 5 items instead of all items that pass the if. In normal code I would add a temp value that counts each addition and later on resets, but I'm new to django and I don't know how to approach this.
Also I can't figure out why my if only works if I cut both names to equal length, even though they should be the same length, shouldn't they?
Also my views:
def index(request):
latest_item_list = Manga.objects.all().order_by('-added_on')[:5]
latest_chapter_list = Chapter.objects.all().order_by('-chapter')
context = {'latest_item_list': latest_item_list,
'latest_chapter_list': latest_chapter_list
}
return render(request, 'Item/index.html', context)
any help or tips would be appreciated!
EDIT, SOLUTION: made new filtered list in views:
latest_chapter_list_short = Chapter.objects.filter(chapter__lt=6)
and iterated through it instead of the full list!
Django has limited functionality in its templates so it's easier to only iterate over the elements that you wish to display. For example, you can prepare the data in the view to only contain five elements:
latest_item_list = Manga.objects.all().order_by('-added_on')[:5]
chapters = Chapter.objects.all().order_by('-chapter')
latest_chapter_per_mange = compute_chapters_per_mange(chapters, latest_item_list)
where compute_chapters_per_mange determines the chapters per Manga model.
Your second question: Django is now comparing the string representation of a Manga object to Manga.name which may not be the same. This could explain why you need truncatechars although I don't fully see why. If you define a __unicode__ method on your Manga model then you can specify how an instance of that model should be displayed as a string (i.e., self.name).

Django: retrieving ManyToManyField objects with minimum set of queries

My code looks like this:
models.py
class Tag(models.Model):
name = models.CharField(max_length=42)
class Post(models.Model):
user = models.ForeignKey(User, related_name='post')
#...various fields...
tags = models.ManyToManyField(Tag, null=True)
views.py
posts = Post.objects.all().values('id', 'user', 'title')
tags_dict = {}
for post in posts: # Iteration? Why?
p = Post.objects.get(pk=[post['id']]) # one extra query? Why?
tags_dict[post['id']] = p.tags.all()
How am I supposed to create a dictionary with tags for each Post object with minimum set of queries? Is it possible to avoid iterating, too?
Yes you will need a loop. But you can save one extra query in each iteration, you don't need to get post object to get all its tags. You can directly query on Tag model to get tags related to post id:
for post in posts:
tags_dict[post['id']] = Tag.objects.filter(post__id=post['id'])
Or use Dict Comprehension for efficiency:
tags_dict = {post['id']: Tag.objects.filter(post__id=post['id']) for post in posts}
If you have Django version >= 1.4 and don't really need a dictionary, but need to cut down the count of queries, you can use this method like this:
posts = Post.objects.all().only('id', 'user', 'title').prefetch_related('tags')
It seems to execute only 2 queries (one for Post and another for Tag with INNER JOIN).
And then you can access post.tags.all without extra queries, because tags was already prefetched.
{% for post in posts %}
{% for tag in post.tags.all %}
{{ tag.name }}
{% endfor %}
{% endfor %}

How to display related content of an object

I have a Contact class that can have many Conversations. But each conversation can belong to a single Contact.
So its a One-To-Many relationship.
class Contact(models.Model):
first_name = models.CharField()
last_name = models.CharField()
class Conversation(models.Model):
contact = models.ForeignKey(Contact)
notes = models.TextField()
def __unicode__(self):
return self.notes
Now when I pass in the contacts to the template, I would like to have one field that shows the last conversation for the contact.
contacts= profile.company.contact_set.all()[:10]
calls = []
for c in contacts:
calls.append(max(c.conversation_set.all()))
And I pass them in like this:
vars = {'contacts' : contacts, 'calls': calls}
variables = RequestContext(request, vars)
return render_to_response('main_page.html', variables)
In the template I came up with this magic:
{% for idx, val in contacts %}
<tr>
<td>...
</td>
<td>
{% if calls %}
{{ calls.idx }}
{% endif %}</td>
</tr>
{% endfor %}
This doesn't show anything for calls. But if I replaced calls.idx with calls.0 I get the first one displayed.
What am I doing wrong? Beside the fact that it could be done probably much easier than that. :) I am open for better ways of doing it.
You can't do this sort of indirect lookup in the template language - calls.idx will always refer to an attribute idx belonging to calls.
However I think a better solution is to add the call value directly onto the contact object - don't forget that in Python objects are dynamic, so you can annotate anything you like onto them.
for c in contacts:
c.max_call = max(c.conversation_set.all())
As an aside, does that max really work? I'm not sure what it would be doing the comparison based on. An alternative is to define get_latest_by in your Conversation model, then you can avoid this loop altogether and just call {{ contact.conversation_set.get_latest }} in your template for each contact through the loop.
(Note this will still be pretty inefficient, there are various ways of getting the whole set of max conversations in one go, but it will do for now.)
class Contact(models.Model):
# your code here
#property
def last_conversation(self):
try:
return self.conversation_set.order_by("-some_date_or_order_field")[0]
except IndexError:
return None
Then you don't have to care about this in your view and just need to call "c.last_contact" in your template.

Django: using aggregates to show vote counts for basic polls app

I'm making a very basic poll app. It's similar to the one in the Django tutorial but I chose to break out the vote counting aspect into its own model (the tutorial just adds a vote count field alongside each answer). Here's my models:
class PollQuestion(models.Model):
question = models.CharField(max_length=75)
class PollAnswer(models.Model):
poll = models.ForeignKey('PollQuestion')
answer = models.CharField(max_length=75)
class PollVote(models.Model):
poll = models.ForeignKey('PollQuestion')
answer = models.ForeignKey('PollAnswer')
date_voted = models.DateTimeField(auto_now_add=True)
user_ip = models.CharField(max_length=75)
I'm trying to show all of the vote counts for a given poll. Here's my view code:
from django.db.models import Count
poll_votes = PollVote.objects.select_related('PollAnswer').filter(poll=poll_id).annotate(num_votes=Count('answer__id'))
When I output the results of this query I just get a single row per vote (eg I see about 40 'answers' for my poll, each one representing a vote for one of the 5 actual PollAnswers). If I look at the queries Django makes, it runs something like this for every vote in the poll:
SELECT `poll_answers`.`id`, `poll_answers`.`poll_id`, `poll_answers`.`answer`
FROM `poll_answers`
WHERE `poll_answers`.`id` = 101
Can anyone poke me in the right direction here? I get the feeling this should be easy.
EDIT: here's my template code, for completeness.
<ul>
{% for vote in votes %}
{{ vote.answer }} ({{ votes.num_votes }})<br />
{% endfor %}
</ul>
Try:
poll_votes = PollVote.objects.filter(poll=poll_id).annotate(num_votes=Count('answer__id'))
or:
poll_votes = PollVote.objects.values('poll', 'answer__answer').filter(poll=poll_id).annotate(num_votes=Count('answer__id'))
Relevant docs:
Django offical docs
Never mind, fixed it myself after finding a tutorial which uses the same sort of models as me.
Essentially the fix was in the view:
p = get_object_or_404(PollQuestion, pk=poll_id)
choices = p.pollanswer_set.all()
And in the template:
{% for choice in choices %}
<p class="resultsList">{{choice.answer}} - {{choice.pollvote_set.count}}</p>
{% endfor %}