Index: django/contrib/thumbnails/base.py
===================================================================
--- django/contrib/thumbnails/base.py (revision 0)
+++ django/contrib/thumbnails/base.py (revision 0)
@@ -0,0 +1,87 @@
+from django.conf import settings
+import os
+from StringIO import StringIO
+from PIL import Image
+from exceptions import ThumbnailNoData, ThumbnailInvalidImage
+from methods import scale
+
+__all__ = ('Thumbnail',)
+
+class Thumbnail(object):
+ method = scale
+ base_url = settings.MEDIA_URL
+ root = settings.MEDIA_ROOT
+
+ def __init__(self, filename='', data=None, overwrite=False, size=None):
+ self._data = data
+ if size:
+ self.size = size
+ self.filename = filename
+ self.overwrite = overwrite
+ if data:
+ # If data was given and the thumbnail does not already exist,
+ # generate thumbnail image now.
+ self.make_thumbnail(data)
+
+ def get_filename(self):
+ return self._filename
+ def set_filename(self, filename):
+ if filename:
+ filename = filename % {'x': self.size[0], 'y': self.size[1], 'method': self.method.__name__}
+ filename = os.path.normpath(filename).lstrip(os.sep)
+ if os.path.splitext(filename)[1] != '.jpg':
+ filename.append('.jpg')
+ filename = os.path.join(self.root, filename)
+ self._filename = filename
+ filename = property(get_filename, set_filename)
+
+ def get_thumbnail(self):
+ if hasattr(self, '_thumbnail'):
+ return self._thumbnail
+ try:
+ img = Image.open(self.filename)
+ except IOError, msg:
+ raise ThumbnailInvalidImage(msg)
+ self._thumbnail = img
+ return img
+ thumbnail = property(get_thumbnail)
+
+ def make_thumbnail(self, data):
+ if self.overwrite or not os.path.isfile(self.filename):
+ try:
+ original = Image.open(StringIO(data))
+ except IOError, msg:
+ raise ThumbnailInvalidImage(msg)
+ self._original_image = original
+ thumbnail = self.method()
+ self._thumbnail = thumbnail
+ if self.filename:
+ thumbnail.save(self.filename, "JPEG")
+
+ def get_url(self):
+ if hasattr(self, '_url'):
+ return self._url
+ filename = self.filename
+ if not os.path.isfile(filename):
+ raise ThumbnailNoData
+ url = os.path.normpath(filename[len(self.root):]).lstrip(os.sep)
+ url = os.path.join(self.base_url, url)
+ if os.sep != '/':
+ url = url.replace(os.sep, '/')
+ self._url = url
+ return url
+ url = property(get_url)
+
+ # Make the object will output it's url to Django templates.
+ __str__ = get_url
+
+ def get_original_image(self):
+ if hasattr(self, '_original_image'):
+ return self._original_image
+ raise ThumbnailNoData
+ original_image = property(get_original_image)
+
+ def delete(self):
+ if os.path.isfile(self.filename):
+ os.remove(self.filename)
+ self.filename = ''
Index: django/contrib/thumbnails/__init__.py
===================================================================
--- django/contrib/thumbnails/__init__.py (revision 0)
+++ django/contrib/thumbnails/__init__.py (revision 0)
@@ -0,0 +1,3 @@
+from base import *
+from exceptions import *
+from methods import *
\ No newline at end of file
Index: django/contrib/thumbnails/exceptions.py
===================================================================
--- django/contrib/thumbnails/exceptions.py (revision 0)
+++ django/contrib/thumbnails/exceptions.py (revision 0)
@@ -0,0 +1,11 @@
+class ThumbnailException(Exception):
+ pass
+
+class ThumbnailNoData(ThumbnailException):
+ pass
+
+class ThumbnailTooSmall(ThumbnailException):
+ pass
+
+class ThumbnailInvalidImage(ThumbnailException):
+ pass
Index: django/contrib/thumbnails/methods.py
===================================================================
--- django/contrib/thumbnails/methods.py (revision 0)
+++ django/contrib/thumbnails/methods.py (revision 0)
@@ -0,0 +1,53 @@
+from PIL import Image
+from exceptions import ThumbnailTooSmall
+
+
+def scale(thumbnail):
+ """ Normal PIL thumbnail """
+ img = thumbnail.original_image
+ size = thumbnail.size
+ if img.size[0] < size[0] and img.size[1] < size[1]:
+ raise ThumbnailTooSmall('Image should be at least %s wide or %s high' % size)
+ img.thumbnail(size, Image.ANTIALIAS)
+ return img
+
+
+def crop(thumbnail):
+ """ Crop the image down to the same ratio as `size` """
+ img = thumbnail.original_image
+ size = thumbnail.size
+
+ if img.size[0] < size[0] or img.size[1] < size[1]:
+ raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size)
+
+ image_x, image_y = img.size
+
+ crop_ratio = size[0] / float(size[1])
+ image_ratio = image_x / float(image_y)
+
+ if crop_ratio < image_ratio:
+ # x needs to shrink
+ top = 0
+ bottom = image_y
+ crop_width = int(image_y * crop_ratio)
+ left = (image_x - crop_width) // 2
+ right = left + crop_width
+ else:
+ # y needs to shrink
+ left = 0
+ right = image_x
+ crop_height = int(image_x * crop_ratio)
+ top = (image_y - crop_height) // 2
+ bottom = top + crop_height
+
+ img = img.crop((left, top, right, bottom))
+ return img.resize(size, Image.ANTIALIAS)
+
+
+def squash(thumbnail):
+ """ Resize the image down to exactly `size` (changes ratio) """
+ img = thumbnail.original_image
+ size = thumbnail.size
+ if img.size[0] < size[0] or img.size[1] < size[1]:
+ raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size)
+ return img.resize(size, Image.ANTIALIAS)
Index: django/contrib/thumbnails/__init__.py
===================================================================
--- django/contrib/thumbnails/__init__.py (revision 0)
+++ django/contrib/thumbnails/__init__.py (revision 0)
@@ -0,0 +1,3 @@
+from base import *
+from exceptions import *
+from methods import *
\ No newline at end of file
Index: django/contrib/thumbnails/base.py
===================================================================
--- django/contrib/thumbnails/base.py (revision 0)
+++ django/contrib/thumbnails/base.py (revision 0)
@@ -0,0 +1,87 @@
+from django.conf import settings
+import os
+from StringIO import StringIO
+from PIL import Image
+from exceptions import ThumbnailNoData, ThumbnailInvalidImage
+from methods import scale
+
+__all__ = ('Thumbnail',)
+
+class Thumbnail(object):
+ method = scale
+ base_url = settings.MEDIA_URL
+ root = settings.MEDIA_ROOT
+
+ def __init__(self, filename='', data=None, overwrite=False, size=None):
+ self._data = data
+ if size:
+ self.size = size
+ self.filename = filename
+ self.overwrite = overwrite
+ if data:
+ # If data was given and the thumbnail does not already exist,
+ # generate thumbnail image now.
+ self.make_thumbnail(data)
+
+ def get_filename(self):
+ return self._filename
+ def set_filename(self, filename):
+ if filename:
+ filename = filename % {'x': self.size[0], 'y': self.size[1], 'method': self.method.__name__}
+ filename = os.path.normpath(filename).lstrip(os.sep)
+ if os.path.splitext(filename)[1] != '.jpg':
+ filename.append('.jpg')
+ filename = os.path.join(self.root, filename)
+ self._filename = filename
+ filename = property(get_filename, set_filename)
+
+ def get_thumbnail(self):
+ if hasattr(self, '_thumbnail'):
+ return self._thumbnail
+ try:
+ img = Image.open(self.filename)
+ except IOError, msg:
+ raise ThumbnailInvalidImage(msg)
+ self._thumbnail = img
+ return img
+ thumbnail = property(get_thumbnail)
+
+ def make_thumbnail(self, data):
+ if self.overwrite or not os.path.isfile(self.filename):
+ try:
+ original = Image.open(StringIO(data))
+ except IOError, msg:
+ raise ThumbnailInvalidImage(msg)
+ self._original_image = original
+ thumbnail = self.method()
+ self._thumbnail = thumbnail
+ if self.filename:
+ thumbnail.save(self.filename, "JPEG")
+
+ def get_url(self):
+ if hasattr(self, '_url'):
+ return self._url
+ filename = self.filename
+ if not os.path.isfile(filename):
+ raise ThumbnailNoData
+ url = os.path.normpath(filename[len(self.root):]).lstrip(os.sep)
+ url = os.path.join(self.base_url, url)
+ if os.sep != '/':
+ url = url.replace(os.sep, '/')
+ self._url = url
+ return url
+ url = property(get_url)
+
+ # Make the object will output it's url to Django templates.
+ __str__ = get_url
+
+ def get_original_image(self):
+ if hasattr(self, '_original_image'):
+ return self._original_image
+ raise ThumbnailNoData
+ original_image = property(get_original_image)
+
+ def delete(self):
+ if os.path.isfile(self.filename):
+ os.remove(self.filename)
+ self.filename = ''
Index: django/contrib/thumbnails/exceptions.py
===================================================================
--- django/contrib/thumbnails/exceptions.py (revision 0)
+++ django/contrib/thumbnails/exceptions.py (revision 0)
@@ -0,0 +1,11 @@
+class ThumbnailException(Exception):
+ pass
+
+class ThumbnailNoData(ThumbnailException):
+ pass
+
+class ThumbnailTooSmall(ThumbnailException):
+ pass
+
+class ThumbnailInvalidImage(ThumbnailException):
+ pass
Index: django/contrib/thumbnails/methods.py
===================================================================
--- django/contrib/thumbnails/methods.py (revision 0)
+++ django/contrib/thumbnails/methods.py (revision 0)
@@ -0,0 +1,53 @@
+from PIL import Image
+from exceptions import ThumbnailTooSmall
+
+
+def scale(thumbnail):
+ """ Normal PIL thumbnail """
+ img = thumbnail.original_image
+ size = thumbnail.size
+ if img.size[0] < size[0] and img.size[1] < size[1]:
+ raise ThumbnailTooSmall('Image should be at least %s wide or %s high' % size)
+ img.thumbnail(size, Image.ANTIALIAS)
+ return img
+
+
+def crop(thumbnail):
+ """ Crop the image down to the same ratio as `size` """
+ img = thumbnail.original_image
+ size = thumbnail.size
+
+ if img.size[0] < size[0] or img.size[1] < size[1]:
+ raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size)
+
+ image_x, image_y = img.size
+
+ crop_ratio = size[0] / float(size[1])
+ image_ratio = image_x / float(image_y)
+
+ if crop_ratio < image_ratio:
+ # x needs to shrink
+ top = 0
+ bottom = image_y
+ crop_width = int(image_y * crop_ratio)
+ left = (image_x - crop_width) // 2
+ right = left + crop_width
+ else:
+ # y needs to shrink
+ left = 0
+ right = image_x
+ crop_height = int(image_x * crop_ratio)
+ top = (image_y - crop_height) // 2
+ bottom = top + crop_height
+
+ img = img.crop((left, top, right, bottom))
+ return img.resize(size, Image.ANTIALIAS)
+
+
+def squash(thumbnail):
+ """ Resize the image down to exactly `size` (changes ratio) """
+ img = thumbnail.original_image
+ size = thumbnail.size
+ if img.size[0] < size[0] or img.size[1] < size[1]:
+ raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size)
+ return img.resize(size, Image.ANTIALIAS)
Index: django/contrib/thumbnails/templatetags/__init__.py
===================================================================
Index: django/contrib/thumbnails/templatetags/thumbnails.py
===================================================================
--- django/contrib/thumbnails/templatetags/thumbnails.py (revision 0)
+++ django/contrib/thumbnails/templatetags/thumbnails.py (revision 0)
@@ -0,0 +1,73 @@
+from django.template import Library
+from django.contrib.thumbnails import Thumbnail, crop, squash, ThumbnailException
+from django.conf import settings
+import os.path
+
+register = Library()
+
+class ThumbnailCrop(Thumbnail):
+ method = crop
+
+class ThumbnailSquash(Thumbnail):
+ method = squash
+
+
+#@register.filter
+def thumbnail(file, size):
+ return create_thumbnail(Thumbnail, file, size)
+register.filter(thumbnail)
+
+
+#@register.filter
+def thumbnail_crop(file, size):
+ return create_thumbnail(ThumbnailCrop, file, size)
+register.filter(thumbnail_crop)
+
+
+#@register.filter
+def thumbnail_squash(file, size):
+ return create_thumbnail(ThumbnailSquash, file, size)
+register.filter(thumbnail_squash)
+
+
+def create_thumbnail(thumbnail_cls, file, size_string):
+ """
+ Creates a thumbnail image for the file (which must exist on MEDIA_ROOT)
+ and returns a url to this image.
+
+ If the thumbnail image is not found, an empty string will be returned.
+ """
+ # Define the size.
+ size = [int(x) for x in size_string.split('x')]
+
+ # Define the filename, then create the thumbnail object.
+ basename, ext = os.path.splitext(file)
+ thumbnail_filename = '%s_%s_%%(method)s%s' % (basename, size_string, ext)
+ original_filename = os.path.join(settings.MEDIA_ROOT, file)
+
+ # See if the thumbnail exists already (and is newer than the
+ # original filename).
+ try:
+ thumbnail = thumbnail_cls(thumbnail_filename, size=size)
+ if os.path.getmtime(original_filename) > os.path.getmtime(thumbnail.filename):
+ thumbnail.delete()
+ else:
+ return thumbnail.url
+ except (ThumbnailException, OSError):
+ # Couldn't get the thumbnail (or something else went wrong).
+ pass
+
+ # Read the original file from disk.
+ try:
+ data = open(original_filename, 'rb').read()
+ except OSError:
+ # Couldn't read the original file.
+ return ''
+
+ # Generate the thumbnail.
+ try:
+ thumbnail = thumbnail_cls(thumbnail_filename, data, size=size)
+ except ThumbnailException:
+ return ''
+
+ return thumbnail
Index: django/contrib/thumbnails/templatetags/__init__.py
===================================================================
Index: django/contrib/thumbnails/templatetags/thumbnails.py
===================================================================
--- django/contrib/thumbnails/templatetags/thumbnails.py (revision 0)
+++ django/contrib/thumbnails/templatetags/thumbnails.py (revision 0)
@@ -0,0 +1,73 @@
+from django.template import Library
+from django.contrib.thumbnails import Thumbnail, crop, squash, ThumbnailException
+from django.conf import settings
+import os.path
+
+register = Library()
+
+class ThumbnailCrop(Thumbnail):
+ method = crop
+
+class ThumbnailSquash(Thumbnail):
+ method = squash
+
+
+#@register.filter
+def thumbnail(file, size):
+ return create_thumbnail(Thumbnail, file, size)
+register.filter(thumbnail)
+
+
+#@register.filter
+def thumbnail_crop(file, size):
+ return create_thumbnail(ThumbnailCrop, file, size)
+register.filter(thumbnail_crop)
+
+
+#@register.filter
+def thumbnail_squash(file, size):
+ return create_thumbnail(ThumbnailSquash, file, size)
+register.filter(thumbnail_squash)
+
+
+def create_thumbnail(thumbnail_cls, file, size_string):
+ """
+ Creates a thumbnail image for the file (which must exist on MEDIA_ROOT)
+ and returns a url to this image.
+
+ If the thumbnail image is not found, an empty string will be returned.
+ """
+ # Define the size.
+ size = [int(x) for x in size_string.split('x')]
+
+ # Define the filename, then create the thumbnail object.
+ basename, ext = os.path.splitext(file)
+ thumbnail_filename = '%s_%s_%%(method)s%s' % (basename, size_string, ext)
+ original_filename = os.path.join(settings.MEDIA_ROOT, file)
+
+ # See if the thumbnail exists already (and is newer than the
+ # original filename).
+ try:
+ thumbnail = thumbnail_cls(thumbnail_filename, size=size)
+ if os.path.getmtime(original_filename) > os.path.getmtime(thumbnail.filename):
+ thumbnail.delete()
+ else:
+ return thumbnail.url
+ except (ThumbnailException, OSError):
+ # Couldn't get the thumbnail (or something else went wrong).
+ pass
+
+ # Read the original file from disk.
+ try:
+ data = open(original_filename, 'rb').read()
+ except OSError:
+ # Couldn't read the original file.
+ return ''
+
+ # Generate the thumbnail.
+ try:
+ thumbnail = thumbnail_cls(thumbnail_filename, data, size=size)
+ except ThumbnailException:
+ return ''
+
+ return thumbnail
Index: docs/thumbnails.txt
===================================================================
--- docs/thumbnails.txt (revision 0)
+++ docs/thumbnails.txt (revision 0)
@@ -0,0 +1,274 @@
+=========================
+django.contrib.thumbnails
+=========================
+
+The ``django.contrib.thumbnails`` package, part of the `"django.contrib" add-ons`_,
+provides a way of thumbnailing images.
+
+It requires the Python Imaging Library (PIL_).
+
+.. _"django.contrib" add-ons: ../add_ons/
+.. _PIL: http://www.pythonware.com/products/pil/
+
+Template filters
+================
+
+To use these template filters, add ``'django.contrib.thumbnails'`` to your
+``INSTALLED_APPS`` setting. Once you've done that, use
+``{% load thumbnails %}`` in a template to give your template access to the
+filters.
+
+The filters, all very similar in behaviour, are:
+
+ * ``thumbnail``
+ * ``thumbnail_crop``
+ * ``thumbnail_squash``
+
+The only difference between them is the `Thumbnail methods`_ that they use.
+
+Using the thumbnail filters
+---------------------------
+
+Usage::
+
+
+
+The filter is applied to a image field (not the url get from
+``get_[field]_url`` method of the model). Supposing the imagefield filename is
+``'image.jpg'``, it creates a thumbnailed image file proportionally resized
+down to a maximum of 150 pixels wide and 100 pixels high called
+``'image_150x100_scale.jpg'`` in the same location as the original image
+and returns the URL to this thumbnail image.
+
+The ``thumbnail_crop`` works exactly the same way but uses the crop method
+(and the filename would be called ``'image_150x100_crop.jpg'``). Similarly,
+``thumbnail_squash`` resizes the image to exactly the dimensions given
+(``'image_150x100_squash.jpg'``).
+
+If the thumbnail filename already exists, it is only overwritten if the date
+of the the original image file is newer than the thumbnail file.
+
+Rather than just outputting the url, you can reference any other properties
+(see the ```Thumbnail`` object properties`_ section below for a complete
+list)::
+
+ {% with object.imagefield|thumbnail:"150x100" as thumb %}
+
+ {% endwith %}
+
+
+Creating a thumbnail
+====================
+
+The rest of this documentation deals with lower-level usage of thumbnails.
+
+To create a thumbnail object, simply call the ``Thumbnail`` class::
+
+ >>> from django.contrib.thumbnails import *
+ >>> thumbnail = Thumbnail(filename, data, size=(100, 100))
+
+The thumbnail object takes the following arguments:
+
+ =============== ===========================================================
+ Argument Description
+ =============== ===========================================================
+
+ ``filename`` A string containing the path and filename to use when
+ saving or retreiving this thumbnail image from disk
+ (relative to the ``Thumbnail`` object's ``root`` property
+ which defaults to ``settings.MEDIA_ROOT``).
+
+ For advanced usage, see the ```Thumbnail```_ property
+ section.
+
+ ``data`` A string or stream of the original image object to be
+ thumbnailed. If not provided and a file matching the
+ thumbnail can not be found, ``TemplateNoData`` will be
+ raised.
+
+ Example::
+
+ >>> Thumbnail('%(method)/s%(x)sx%(y)s/test.jpg', size=(60, 40)).filename
+ '.../scale/60x40/test.jpg'
+
+ ``overwrite`` Set to ``True`` to overwrite the thumbnail with ``data``
+ even if an existing cached thumbnail is found. Defaults to
+ ``False``.
+
+ ``size`` The size for the thumbnail image. Required unless using a
+ subclass which provides a default ``size`` (see the
+ `Custom thumbnails`_ section below).
+
+``Thumbnail`` object properties
+===============================
+
+The thumbnail object which is created provides the following properties and
+functions:
+
+``filename``
+------------
+
+Reading this property returns the full path and filename to this thumbnail
+image.
+
+When you set this property, the filename string you provide is internally
+appended to the ``Thumbnail`` object's ``root`` property.
+
+You can use string formatting to generate the filename based on the
+thumbnailing method and size:
+
+ * ``%(x)s`` for the thumbnail target width,
+ * ``%(y)s`` for the thumbnail target height,
+ * ``%(method)s`` for the thumbnailing method.
+
+For example::
+
+ >>> Thumbnail('%(media)/s%(x)sx%(y)s/test.jpg', size=(60, 40)).filename
+ '.../scale/60x40/test.jpg'
+
+Note: thumbnailed images are always saved as JPEG images, so if the filename
+string does not end in `'.jpg'`, this will be automatically appended to the
+thumbnail's filename.
+
+``original_image``
+------------------
+
+This read-only property returns a PIL ``Image`` containing the original
+image (passed in with ``data``).
+
+``thumbnail``
+-------------
+
+This read-only property returns a PIL ``Image`` containing the thumbnail
+image.
+
+``url``
+-------
+
+This read-only property returns the full url this thumbnail image.
+
+It is generated by appending the parsed ``filename`` string to the
+``Template`` object's ``base_url`` property.
+
+``delete()``
+------------
+
+Call this function to delete the thumbnail file if it exists on the disk.
+
+Custom thumbnails
+=================
+
+Similar to newforms, you can create a subclass to override the default
+properties of the ``Thumbnail`` base class::
+
+ from django.contrib.thumbnails import Thumbnail
+
+ class MyThumbnail(Thumbnail):
+ size = (100, 100)
+
+Here are the properties you can provide to your subclass:
+
+ =============== ===========================================================
+ Property Description
+ =============== ===========================================================
+
+ ``size`` Default size for creating thumbnails (no default).
+ ``base_url`` Base url for thumbnails (default is ``settings.MEDIA_URL``).
+ ``root`` Base directory for thumbnails (default is
+ ``settings.MEDIA_ROOT``).
+ ``method`` The thumbnailing funciton to use (default is ``scale``).
+ See the `Thumbnail methods`_ section below.
+
+Thumbnail methods
+=================
+
+There are two thumbnailing methods available in
+``django.contrib.thumbnails.methods``
+
+``crop()``
+----------
+
+This method crops the image height or width to match the ratio of the thumbnail
+``size`` and then resizes it down to exactly the dimensions of ``size``.
+
+It requires the original image to be both as wide and as high as ``size``.
+
+``scale()``
+-----------
+
+This is the normal PIL scaling method of proportionally resizing the image down
+to no greater than the thumbnail ``size`` dimensions.
+
+It requires the original image to be either as wide or as high as ``size``.
+
+``squash()``
+------------
+
+This method resizes the image down to exactly the dimensions given. This will
+potentially squash or stretch the image.
+
+It requires the original image to be both as wide and as high as ``size``.
+
+Making your own methods
+-----------------------
+
+To make your own thumbnailing function, create a function which accepts one
+parameter (``thumbnail``) and returns a PIL ``Image``.
+
+The ``thumbnail`` parameter will be a ``Thumbnail`` object, so you can use it
+to get the original image (it will raise ``ThumbnailNoData`` if no data was
+provided) and the thumbnail size::
+
+ img = thumbnail.original_image
+ size = thumbnail.size
+
+Exceptions
+==========
+
+The following exceptions (all found in ``django.contrib.thumbnails.exceptions``
+and all subclasses of ``ThumbnailException``) could be raised when using the
+``Thumbnail`` object:
+
+ ========================= ================================================
+ Exception Reason
+ ========================= ================================================
+
+ ``ThumbnailNoData`` Tried to get the ``original_image`` when no
+ ``data`` was provided or tried to get the
+ ``url`` when the file did not exist and no
+ ``data`` was provided.
+ ``ThumbnailTooSmall`` The ``original_image`` was too small to
+ thumbnail using the given thumbnailing method.
+ ``ThumbnailInvalidImage`` The ``data`` provided could not be decoded to
+ a valid image format (or more rarely, using
+ ``thumbnail`` to retreive an existing thumbnail
+ file from disk which could not be decoded to a
+ valid image format).
+
+Putting it all together
+=======================
+
+Here is a snippet of an example view which receives an image file from the user
+and saves a thumbnail of this image to a file named ``[userid].jpg``::
+
+ from django.contrib.thumbnails import Thumbnail, crop, ThumbnailException
+
+ class ProfileThumbnail(Thumbnail):
+ size = (100, 100)
+ method = crop
+
+ def profile_image(request, id):
+ profile = get_object_or_404(Profile, pk=id)
+ if request.method == 'POST':
+ image = request.FILES.get('profile_image')
+ profile.has_image = False
+ if image:
+ filename = str(profile.id)
+ try:
+ thumbnail = ProfileThumbnail(filename, image['content'])
+ profile.has_image = True
+ except ThumbnailException:
+ pass
+ profile.save()
+ return HttpResponseRedirect('../')
+ ...