views:

339

answers:

2

Hey,

I have a model Book which can be added to user's Favorites. So I defined 2 classes:


class Book(models.Model):
    name = models.CharField(max_length = 40)

class UserFavorite(models.Model):
    book = models.ForeignKey(Book)
    user = models.ForeignKey(User)

Now I'd like to show on the main page popular books with an icon either "add to favorites" or "remove from favorites". Basically I pass array of popular books to a template and from the template I loop thru this array and should say "if book.is_in_favorites(request.user): ...", but this is not possible from template.

A: 

I don't understand what this has to do with templates. You just need to display a link to a view that takes the book id as a parameter, gets the user from the request, and adds a UserFavourite using the two.

Update after edit: OK, I understand your issue now. Well, as you've found, this sort of thing is too complex to put into template logic. It really needs to go either in your view, or in a template tag. Probably a template tag is best, as you will no doubt want to use this functionality in multiple views/templates.

You will probably want to use an inclusion tag, as that has the benefit of being able to take the context automatically. Something like (untested):

@register.inclusion_tag('add_to_favourites.html', takes_context=True)
def favourite_links(context):
    user = context['request'].user
    books = Book.objects.exclude(userfavourite__in=user)
    return {'books': books, 'user':user}

Now you just need a template fragment which renders a list using the values in books returned by the tag above. And in your main template, just use favourite_links to show the book links.

Daniel Roseman
Basically I pass an array of popular books to a template from the main view. Now from the template I loop thru this array and should say smth like "if book.is_in_favorites(request.user)" -- which is not possible this way. I don't care for now how to implement view which will add this book into favorites.
Vitaly
+3  A: 

The most common approach to this would be to decorate the book objects in the view. In the view code, loop over the books, adding a boolean indicating whether they are a favorite of the current user or not:

for b in books:
    b.is_fav = b.is_in_favorites(request.user)

then in the template:

{% for b in books %}
    blah blah {{b.blah}}
    {% if b.is_fav %}FAVORITE INDICATOR{% endif %}
{% endfor %}

If you don't like modifying the book objects, you can make a list of dicts that wraps up books with their annotations:

book_info = [{'book':b, 'is_fav':b.is_in_favorites(request.user)} for b in books]

and use it in your template:

{% for bi in book_info %}
    blah blah {{b.book.blah}}
    {% if bi.is_fav %}FAVORITE INDICATOR{% endif %}
{% endfor %}
Ned Batchelder
1) Do I have to define is_fav in the Book model? Or this will add a field "on the fly"?2) Approach is fine, although it takes many select statements to run (one for all books, and then 1 for each book for is_fav), which affects performance. May be there's any way to do that with just 1 query which gets all the books?
Vitaly
Actually I found that there's "extra" clause which can add properties on the fly, I will try to utilize it
Vitaly
If you wanted to avoid the repeated lookups caused by the for statement (and since select_related *won't* help here) you call UserFavorites.objects.filter(user=request.user) and compare your iteration of books to the set of UserFavorites returned. Then it's only 2 DB hits.
Gabriel Hurley