Opened 5 years ago

Closed 5 years ago

Last modified 5 years ago

#13848 closed (worksforme)

Template filter bug when accessing foo_set manager.

Reported by: danols@… Owned by: nobody
Component: Template system Version: 1.2
Severity: Keywords: Related objects
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: UI/UX:

Description (last modified by Alex)

Hi Awesome Django Team,

Below is a bug that I've discovered (I did not find anything similiar in the bug list). The best way I understand it is that when using foo.bar_set.all.0 with a filter the filter is called twice. First with the bar object being passed and then second with the bar object as string.

Below is the code to duplicate it:

class Foo(models.Model):
    title = models.CharField(max_length=50)
    
class Photo(models.Model):
    image = models.ImageField(upload_to='')

In a view I pass all Foos objects to the template. And attempt to retrieve just the first image to be displayed but first it is passed to a filter to create a thubmnail.

Here is the rough template code that generates the error: Caught AttributeError while rendering: 'str' object has no attribute 'path'.

{% for foo in foos %}
   <img alt='image' src='{{ foo.photo_set.all.0.image|thumbnail:'200x200 }}
{% endfor %}

The thumbnail filter is called twice - print typeof(file) statement in the filter outputs:

<class 'django.db.models.fields.files.ImageFieldFile'>
<type 'str'>

A workarround is insert another loop like this:

{% for foo in foos %}
   {% for photo in foo.photo_set.all %}
      {% if forloop.first %}
          <img alt='image' src='{{ photo.image|thumbnail:'200x200 }}
      {% endif %}
   {% endfor %}
{% endfor %}

Thank you for looking into it.


# Author Daniel Sokolowski (danols@danols.com)
#
# Original see http://www.djangosnippets.org/snippets/955/
# 
# A filter to resize a ImageField on demand, a use case could be:
# <img src="{{ object.image.url }}" alt="original image">
# <img src="{{ object.image|thumbnail }}" alt="image resized to default 104x104 format">
# <img src="{{ object.image|thumbnail:'200x300' }}" alt="image resized to 200x300">
# not implemented <img src="{{ object.image|thumbnail:'200x300_crop' }}" alt="image resize to 200x300 with centered croping"
# cropping on by deafult!

import os
import Image
from django.template import Library
import json

register = Library()

def thumbnail(imagefield, options='104x104'):
    print type(imagefield)
    # defining the size
    x, y = [int(x) for x in options.split('x')]
    # defining the filename and the miniature filename
    filehead, filetail = os.path.split(imagefield.path)
    basename, format = os.path.splitext(filetail)
    miniature = basename + '_' + options + format
    filename = imagefield.path
    miniature_filename = os.path.join(filehead, miniature)
    filehead, filetail = os.path.split(imagefield.url)
    miniature_url = filehead + '/' + miniature
    if os.path.exists(miniature_filename) and os.path.getmtime(filename)>os.path.getmtime(miniature_filename):
        os.unlink(miniature_filename)
    # if the image wasn't already resized, resize it
    if not os.path.exists(miniature_filename):
        image = Image.open(filename)

        # crop if specified.
        if (False):
            # find out the x1,x2,y1,y2 cordinates for the croping of the image
            # we are going to center this ractangel.
            int_width = image.size[0]
            int_height = image.size[1]
            # find out biggest square size
            int_box_side = 0
            if int_width < int_height:
                int_box_side = int_width
            else:
                int_box_side = int_height
            # fidn out the offset
            int_width_offset = (int_width - int_box_side) / 2 # since two sides
            int_height_offset = (int_height - int_box_side) / 2

            # crop it
            image = image.crop([int_width_offset,int_height_offset,int_width-int_width_offset,int_height-int_height_offset])
        
        
       
        image.thumbnail([x, y], Image.ANTIALIAS)
        try:
            image.save(miniature_filename, image.format, quality=90, optimize=1)
        except:
            image.save(miniature_filename, image.format, quality=90)
    return miniature_url

register.filter(thumbnail)

Change History (4)

comment:1 Changed 5 years ago by Alex

  • Description modified (diff)

Reformated code, please use preview in the future.

comment:2 Changed 5 years ago by anonymous

  • Resolution set to worksforme
  • Status changed from new to closed

First, things I have to change to make the example running:

class Foo(models.Model):
    title = models.CharField(max_length=50)
    
class Photo(models.Model):
    foo = models.ForeignKey(Foo) # added this line
    image = models.ImageField(upload_to='anywhere') # upload_to can't be empty, models won't validate
{% for foo in foos %}
   <img alt='image' src='{{ foo.photo_set.all.0.image|thumbnail:'200x200' }} {# missing apostrophe, was: '200x200 #}
{% endfor %}

Now I added a Foo object with a few related Photo objects. The filter run only once and <class 'django.db.models.fields.files.ImageFieldFile'> was printed. Then I added a Foo object *without* a Photo object. The filter correctly run twice - once for each Foo. First with ImageFieldFile as argument, second time with string argument.

This is correct behaviour: because foo.photo_set.all.0.image is not a valid expression (trying to get an element from an empty queryset yields an IndexError), Django template system replaces it with an empty string, which then is passed to the filter. See http://docs.djangoproject.com/en/dev/ref/templates/api/#how-invalid-variables-are-handled and the example above that paragraph.

Your "workaround" worked, because for on empty iterable will omit it's content entirely. Instead you can just fix your filter to check if imagefield argument is valid:

def thumbnail(imagefield, options='104x104'):
    if not imagefield:
        return ''
    # rest of the code 

comment:3 Changed 5 years ago by lrekucki

... and of course I forgot to login ;)

comment:4 Changed 5 years ago by danols@…

Wow,

It's so obvious now!

Thank a ton Lrekucki!

Note: See TracTickets for help on using tickets.
Back to Top