#13848 closed (worksforme)
Template filter bug when accessing foo_set manager.
Reported by: | 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: | no | UI/UX: | no |
Description (last modified by )
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 by , 14 years ago
Description: | modified (diff) |
---|
comment:2 by , 14 years ago
Resolution: | → worksforme |
---|---|
Status: | new → 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
Reformated code, please use preview in the future.