<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>django_openidauth/admin.py</filename>
    </added>
    <added>
      <filename>djangopeople/admin.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/defaults.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/fields.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/management/__init__.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/management/commands/__init__.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/management/commands/thumbnail_cleanup.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/processors.py</filename>
    </added>
    <added>
      <filename>lib/sorl/thumbnail/tests/fields.py</filename>
    </added>
    <added>
      <filename>lib/tagging/admin.py</filename>
    </added>
    <added>
      <filename>lib/tagging/generic.py</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -1,2 +1,3 @@
 people.db
 _thumbs
+djangopeoplenet/static/profiles/</diff>
      <filename>.gitignore</filename>
    </modified>
    <modified>
      <diff>@@ -27,3 +27,8 @@ simplify QS reduce given QS.none
 make SkillsForm work (just bring in updated tagging?)
 
 
+404s:
+	je.gif
+	aq.gif
+
+rewrite raw sql using aggregates, where possible.</diff>
      <filename>README</filename>
    </modified>
    <modified>
      <diff>@@ -9,8 +9,6 @@ class UserOpenID(models.Model):
     created_at = models.DateTimeField()
     def __unicode__(self):
         return &quot;&lt;User %s has OpenID %s&gt;&quot; % (self.user, self.openid)
-    class Admin:
-        pass
     class Meta:
         ordering = ('-created_at',)
 </diff>
      <filename>django_openidauth/models.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,4 @@
-from django import newforms as forms
+from django import forms
 from django.shortcuts import render_to_response
 from django.http import HttpResponseRedirect
 from django.template import RequestContext
@@ -67,7 +67,7 @@ def register(request, success_url='/accounts/register/complete/',
                               context_instance=RequestContext(request))
 
 def demo_delete_me_asap(request):
-    import django.newforms as forms
+    import django.forms
     
     class UserProfileForm(forms.Form):
         name = forms.CharField(max_length=100)
@@ -76,7 +76,7 @@ def demo_delete_me_asap(request):
         dob = forms.DateField(required=False)
         receive_newsletter = forms.BooleanField(required=False)
         def clean_email(self):
-            from django.newforms.util import ValidationError
+            from django.forms.util import ValidationError
             if self.cleaned_data['email'].split('@')[1] == 'hotmail.com':
                 raise ValidationError, &quot;No hotmail.com emails, please.&quot;
 </diff>
      <filename>django_openidauth/regviews.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,5 +1,5 @@
-from django import newforms as forms
-from django.newforms.forms import BoundField
+from django import forms
+from django.forms.forms import BoundField
 from django.db.models import ObjectDoesNotExist
 from djangopeople.models import DjangoPerson, Country, Region, User, RESERVED_USERNAMES
 from djangopeople.groupedselect import GroupedChoiceField</diff>
      <filename>djangopeople/forms.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,5 @@
-from django import newforms as forms
+from django import forms
 from django.utils.safestring import mark_safe
-
 # From http://www.djangosnippets.org/snippets/200/
 
 # widget for select with optional opt groups
@@ -25,7 +24,7 @@ from django.utils.safestring import mark_safe
 class GroupedSelect(forms.Select): 
     def render(self, name, value, attrs=None, choices=()):
         from django.utils.html import escape
-        from django.newforms.util import flatatt, smart_unicode 
+        from django.forms.util import flatatt, smart_unicode
         if value is None: value = '' 
         final_attrs = self.build_attrs(attrs, name=name) 
         output = [u'&lt;select%s&gt;' % flatatt(final_attrs)] </diff>
      <filename>djangopeople/groupedselect.py</filename>
    </modified>
    <modified>
      <diff>@@ -107,9 +107,6 @@ class Country(models.Model):
     def __unicode__(self):
         return self.name
     
-    class Admin:
-        pass
-
 class Region(models.Model):
     code = models.CharField(max_length=20)
     name = models.CharField(max_length=50)
@@ -132,9 +129,6 @@ class Region(models.Model):
     class Meta:
         ordering = ('name',)
     
-    class Admin:
-        pass
-
 class DjangoPerson(models.Model):
     user = models.ForeignKey(User, unique=True)
     bio = models.TextField(blank=True)
@@ -213,9 +207,9 @@ class DjangoPerson(models.Model):
     def get_absolute_url(self):
         return '/%s/' % self.user.username
     
-    def save(self): # TODO: Put in transaction
+    def save(self, force_insert=False, force_update=False): # TODO: Put in transaction
         # Update country and region counters
-        super(DjangoPerson, self).save()
+        super(DjangoPerson, self).save(force_insert=False, force_update=False)
         self.country.num_people = self.country.djangoperson_set.count()
         self.country.save()
         if self.region:
@@ -225,9 +219,6 @@ class DjangoPerson(models.Model):
     class Meta:
         verbose_name_plural = 'Django people'
 
-    class Admin:
-        list_display = ('user', 'profile_views')
-
     def irc_tracking_allowed(self):
         return not self.machinetags.filter(
             namespace = 'privacy', predicate='irctrack', value='private'
@@ -246,9 +237,6 @@ class PortfolioSite(models.Model):
     def __unicode__(self):
         return '%s &lt;%s&gt;' % (self.title, self.url)
     
-    class Admin:
-        pass
-
 class CountrySite(models.Model):
     &quot;Community sites for various countries&quot;
     title = models.CharField(max_length = 100)
@@ -258,26 +246,23 @@ class CountrySite(models.Model):
     def __unicode__(self):
         return '%s &lt;%s&gt;' % (self.title, self.url)
    
-    class Admin:
-        pass
-
-class ClusteredPoint(models.Model):
-    
-    &quot;&quot;&quot;
-    Represents a clustered point on the map. Each cluster is at a lat/long,
-    is only for one zoom level, and has a number of people.
-    If it is only one person, it is also associated with a DjangoPerson ID.
-    &quot;&quot;&quot;
-    
-    latitude = models.FloatField()
-    longitude = models.FloatField()
-    zoom = models.IntegerField()
-    number = models.IntegerField()
-    djangoperson = models.ForeignKey(DjangoPerson, blank=True, null=True)
-    
-    def __unicode__(self):
-        return &quot;%s people at (%s,%s,z%s)&quot; % (self.number, self.longitude, self.latitude, self.zoom)
-    
-    class Admin:
-        list_display = (&quot;zoom&quot;, &quot;latitude&quot;, &quot;longitude&quot;, &quot;number&quot;)
-        ordering = (&quot;zoom&quot;,)
+#class ClusteredPoint(models.Model):
+#    
+#    &quot;&quot;&quot;
+#    Represents a clustered point on the map. Each cluster is at a lat/long,
+#    is only for one zoom level, and has a number of people.
+#    If it is only one person, it is also associated with a DjangoPerson ID.
+#    &quot;&quot;&quot;
+#    
+#    latitude = models.FloatField()
+#    longitude = models.FloatField()
+#    zoom = models.IntegerField()
+#    number = models.IntegerField()
+#    djangoperson = models.ForeignKey(DjangoPerson, blank=True, null=True)
+#    
+#    def __unicode__(self):
+#        return &quot;%s people at (%s,%s,z%s)&quot; % (self.number, self.longitude, self.latitude, self.zoom)
+#    
+#    class Admin:
+#        list_display = (&quot;zoom&quot;, &quot;latitude&quot;, &quot;longitude&quot;, &quot;number&quot;)
+#        ordering = (&quot;zoom&quot;,)</diff>
      <filename>djangopeople/models.py</filename>
    </modified>
    <modified>
      <diff>@@ -279,7 +279,7 @@ def upload_profile_photo(request, username):
         form = PhotoUploadForm(request.POST, request.FILES)
         if form.is_valid():
             # Figure out what type of image it is
-            image_content = request.FILES['photo']['content']
+            image_content = request.FILES['photo'].read()
             format = Image.open(StringIO(image_content)).format
             format = format.lower().replace('jpeg', 'jpg')
             filename = md5.new(image_content).hexdigest() + '.' + format</diff>
      <filename>djangopeople/views.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,5 @@
 from django.conf.urls.defaults import *
+from django.contrib import admin
 from django.conf import settings
 from django.http import HttpResponseRedirect
 from djangopeople import views, api #, clustering
@@ -9,6 +10,8 @@ import os
 def redirect(url):
     return lambda res: HttpResponseRedirect(url)
 
+admin.autodiscover()
+
 urlpatterns = patterns('',
     (r'^$', views.index),
     (r'^login/$', views.login),
@@ -38,10 +41,9 @@ urlpatterns = patterns('',
     (r'^static/(?P&lt;path&gt;.*)$', 'django.views.static.serve', {
         'document_root': os.path.join(settings.OUR_ROOT, 'static')
     }),
-    (r'^admin/', include('django.contrib.admin.urls')),
-   
+    (r'^admin/(.*)', admin.site.root),
     (r'^api/irc_lookup/(.*?)/$', api.irc_lookup),
-    (r'^api/irc_spotted/(.*?)/$', api.irc_spotted), 
+    (r'^api/irc_spotted/(.*?)/$', api.irc_spotted),
     (r'^irc/active/$', views.irc_active),
     (r'^irc/(.*?)/$', api.irc_redirect),
     </diff>
      <filename>djangopeoplenet/urls.py</filename>
    </modified>
    <modified>
      <filename>lib/sorl/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,74 +1,65 @@
 import os
-from os import makedirs
 from os.path import isfile, isdir, getmtime, dirname, splitext, getsize
-from PIL import Image, ImageFilter
-from methods import autocrop, resize_and_crop
-from subprocess import Popen, PIPE
 from tempfile import mkstemp
+from shutil import copyfile
+from subprocess import Popen, PIPE
 
+from PIL import Image, ImageFilter
 
-# Valid options for the Thumbnail class.
-VALID_OPTIONS = ['crop', 'autocrop', 'upscale', 'bw', 'detail', 'sharpen']
+from sorl.thumbnail import defaults
+from sorl.thumbnail.processors import get_valid_options, dynamic_import
 
 
 class ThumbnailException(Exception):
-    pass
+    # Stop Django templates from choking if something goes wrong.
+    silent_variable_failure = True
 
 
 class Thumbnail(object):
     def __init__(self, source, requested_size, opts=None, quality=85,
-                 dest=None, imagemagick_path='/usr/bin/convert',
-                 wvps_path='/usr/bin/wvPS'):
-        # Converter paths
-        self.imagemagick_path = imagemagick_path
+                 dest=None, convert_path=defaults.CONVERT,
+                 wvps_path=defaults.WVPS, processors=None):
+        # Paths to external commands
+        self.convert_path = convert_path
         self.wvps_path = wvps_path
         # Absolute paths to files
         self.source = source
         self.dest = dest
-        
-        # Ensure the source file exists
-        if not isfile(self.source):
-            raise ThumbnailException('Source file does not exist')
 
-        # Set the source type
-        try:
-            import magic
-            m = magic.open(magic.MAGIC_NONE)
-            m.load()
-            ftype = m.file(self.source)
-            if ftype.find('Microsoft Office Document') != -1:
-                self.source_type = 'doc'
-            elif ftype.find('PDF document') != -1:
-                self.source_type = 'pdf'
-            else:
-                self.source_type = ftype
-        except:
-            self.source_type = splitext(self.source)[1].lower().replace('.', '')
-        
         # Thumbnail settings
-        self.requested_size = requested_size
-        if not 0 &lt; quality &lt;= 100:
+        try:
+            x, y = [int(v) for v in requested_size]
+        except (TypeError, ValueError):
+            raise TypeError('Thumbnail received invalid value for size '
+                            'argument: %s' % repr(requested_size))
+        else:
+            self.requested_size = (x, y)
+        try:
+            self.quality = int(quality) 
+            if not 0 &lt; quality &lt;= 100:
+                raise ValueError
+        except (TypeError, ValueError):
             raise TypeError('Thumbnail received invalid value for quality '
-                            'argument: %s' % quality)
-        self.quality = quality
+                            'argument: %r' % quality)
+
+        # Processors
+        if processors is None:
+            processors = dynamic_import(defaults.PROCESSORS)
+        self.processors = processors
+
+        # Handle old list format for opts.
+        opts = opts or {}
+        if isinstance(opts, (list, tuple)):
+            opts = dict([(opt, None) for opt in opts])
 
         # Set Thumbnail opt(ion)s
-        opts = opts or []
-        # First we check that all options received are valid
+        VALID_OPTIONS = get_valid_options(processors)
         for opt in opts:
             if not opt in VALID_OPTIONS:
                 raise TypeError('Thumbnail received an invalid option: %s'
                                 % opt)
-        # Then we populate the opts dict and the (sorted) opts list
-        self.opts = {}
-        self.opts_list = []
-        for opt in VALID_OPTIONS:
-            if opt in opts:
-                self.opts[opt] = True
-                self.opts_list.append(opt) # cheap sorted list with options
-            else:
-                self.opts[opt] = False
-                
+        self.opts = opts
+
         if self.dest is not None:
             self.generate()
 
@@ -81,16 +72,61 @@ class Thumbnail(object):
         if not self.dest:
             raise ThumbnailException(&quot;No destination filename set.&quot;)
 
-        if not isfile(self.dest) or \
-           getmtime(self.source) &gt; getmtime(self.dest):
-            
+        if not isinstance(self.dest, basestring):
+            # We'll assume dest is a file-like instance if it exists but isn't
+            # a string.
+            self._do_generate()
+        elif not isfile(self.dest) or (self.source_exists and
+            getmtime(self.source) &gt; getmtime(self.dest)):
+
             # Ensure the directory exists
             directory = dirname(self.dest)
-            if not isdir(directory):
-                makedirs(directory)
-            
+            if directory and not isdir(directory):
+                os.makedirs(directory)
+
             self._do_generate()
 
+    def _check_source_exists(self):
+        &quot;&quot;&quot;
+        Ensure the source file exists. If source is not a string then it is
+        assumed to be a file-like instance which &quot;exists&quot;.
+        &quot;&quot;&quot;
+        if not hasattr(self, '_source_exists'):
+            self._source_exists = (self.source and
+                                   (not isinstance(self.source, basestring) or
+                                    isfile(self.source)))
+        return self._source_exists
+    source_exists = property(_check_source_exists)
+
+    def _get_source_filetype(self):
+        &quot;&quot;&quot;
+        Set the source filetype. First it tries to use magic and
+        if import error it will just use the extension
+        &quot;&quot;&quot;
+        if not hasattr(self, '_source_filetype'):
+            if not isinstance(self.source, basestring):
+                # Assuming a file-like object - we won't know it's type.
+                return None
+            try:
+                import magic
+            except ImportError:
+                self._source_filetype = splitext(self.source)[1].lower().\
+                   replace('.', '').replace('jpeg', 'jpg')
+            else:
+                m = magic.open(magic.MAGIC_NONE)
+                m.load()
+                ftype = m.file(self.source)
+                if ftype.find('Microsoft Office Document') != -1:
+                    self._source_filetype = 'doc'
+                elif ftype.find('PDF document') != -1:
+                    self._source_filetype = 'pdf'
+                elif ftype.find('JPEG') != -1:
+                    self._source_filetype = 'jpg'
+                else:
+                    self._source_filetype = ftype
+        return self._source_filetype
+    source_filetype = property(_get_source_filetype)
+
     # data property is the image data of the (generated) thumbnail
     def _get_data(self):
         if not hasattr(self, '_data'):
@@ -106,22 +142,27 @@ class Thumbnail(object):
     # source_data property is the image data from the source file
     def _get_source_data(self):
         if not hasattr(self, '_source_data'):
-            if self.source_type == 'doc':
+            if not self.source_exists:
+                raise ThumbnailException(&quot;Source file: '%s' does not exist.&quot; %
+                                         self.source)
+            if self.source_filetype == 'doc':
                 self._convert_wvps(self.source)
-            elif self.source_type == 'pdf':
+            elif self.source_filetype == 'pdf':
                 self._convert_imagemagick(self.source)
             else:
                 self.source_data = self.source
         return self._source_data
-    
+
     def _set_source_data(self, image):
         if isinstance(image, Image.Image):
             self._source_data = image
-        else: 
+        else:
             try:
                 self._source_data = Image.open(image)
             except IOError, detail:
-                raise ThumbnailException(detail)
+                raise ThumbnailException(&quot;%s: %s&quot; % (detail, image))
+            except MemoryError:
+                raise ThumbnailException(&quot;Memory Error: %s&quot; % image)
     source_data = property(_get_source_data, _set_source_data)
 
     def _convert_wvps(self, filename):
@@ -130,63 +171,66 @@ class Thumbnail(object):
             p = Popen((self.wvps_path, filename, tmp), stdout=PIPE)
             p.wait()
         except OSError, detail:
+            os.remove(tmp)
             raise ThumbnailException('wvPS error: %s' % detail)
-        self._convert_image_imagemagick(tmp)
+        self._convert_imagemagick(tmp)
         os.remove(tmp)
 
     def _convert_imagemagick(self, filename):
         tmp = mkstemp('.png')[1]
-        if self.opts['crop']:
-            x,y = [d*3 for d in self.requested_size]
+        if 'crop' in self.opts or 'autocrop' in self.opts:
+            x, y = [d * 3 for d in self.requested_size]
         else:
-            x,y = self.requested_size
+            x, y = self.requested_size
         try:
-            p = Popen((self.imagemagick_path, '-size', '%sx%s' % (x,y),
-                '-antialias', '-colorspace', 'rgb', '-format', 'PNG32',
+            p = Popen((self.convert_path, '-size', '%sx%s' % (x, y),
+                '-antialias', '-colorspace', 'rgb', '-format', 'PNG24',
                 '%s[0]' % filename, tmp), stdout=PIPE)
             p.wait()
         except OSError, detail:
+            os.remove(tmp)
             raise ThumbnailException('ImageMagick error: %s' % detail)
         self.source_data = tmp
         os.remove(tmp)
-    
+
     def _do_generate(self):
         &quot;&quot;&quot;
         Generates the thumbnail image.
-        
+
         This a semi-private method so it isn't directly available to template
         authors if this object is passed to the template context.
         &quot;&quot;&quot;
         im = self.source_data
 
-        if self.opts['bw'] and im.mode != &quot;L&quot;:
-            im = im.convert(&quot;L&quot;)
-        elif im.mode not in (&quot;L&quot;, &quot;RGB&quot;):
-            im = im.convert(&quot;RGB&quot;)
-
-        if self.opts['autocrop']:
-            im = autocrop(im)
-
-        im = resize_and_crop(im, self.requested_size, self.opts['upscale'],
-                             self.opts['crop'])
+        for processor in self.processors:
+            im = processor(im, self.requested_size, self.opts)
 
-        if self.opts['detail']:
-            im = im.filter(ImageFilter.DETAIL)
-
-        if self.opts['sharpen']:
-            im = im.filter(ImageFilter.SHARPEN)
-        
         self.data = im
-        try:
-            im.save(self.dest, &quot;JPEG&quot;, quality=self.quality, optimize=1)
-        except IOError:
-            # Try again, without optimization (the JPEG library can't optimize
-            # an image which is larger than ImageFile.MAXBLOCK which is 64k by
-            # default)
+
+        filelike = not isinstance(self.dest, basestring)
+        if not filelike:
+            dest_extension = os.path.splitext(self.dest)[1][1:]
+            format = None
+        else:
+            dest_extension = None
+            format = 'JPEG'
+        if (self.source_filetype and self.source_filetype == dest_extension and
+                self.source_data == self.data):
+            copyfile(self.source, self.dest)
+        else:
             try:
-                im.save(self.dest, &quot;JPEG&quot;, quality=self.quality)
-            except IOError, detail:
-                raise ThumbnailException(detail)
+                im.save(self.dest, format=format, quality=self.quality,
+                        optimize=1)
+            except IOError:
+                # Try again, without optimization (PIL can't optimize an image
+                # larger than ImageFile.MAXBLOCK, which is 64k by default)
+                try:
+                    im.save(self.dest, format=format, quality=self.quality)
+                except IOError, detail:
+                    raise ThumbnailException(detail)
+
+        if filelike:
+            self.dest.seek(0)
 
     # Some helpful methods
 
@@ -208,9 +252,9 @@ class Thumbnail(object):
             self._filesize = getsize(self.dest)
         return self._filesize
     filesize = property(_get_filesize)
-   
+
     def _source_dimension(self, axis):
-        if self.source_type in ['pdf', 'doc']:
+        if self.source_filetype in ['pdf', 'doc']:
             return None
         else:
             return self.source_data.size[axis]
@@ -220,7 +264,7 @@ class Thumbnail(object):
 
     def source_height(self):
         return self._source_dimension(1)
-    
+
     def _get_source_filesize(self):
         if not hasattr(self, '_source_filesize'):
             self._source_filesize = getsize(self.source)</diff>
      <filename>lib/sorl/thumbnail/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,60 +1,110 @@
 import os
+
 from django.conf import settings
-from django.utils.encoding import iri_to_uri
-from base import Thumbnail
-from utils import get_thumbnail_setting
+from django.utils.encoding import iri_to_uri, force_unicode
+
+from sorl.thumbnail.base import Thumbnail
+from sorl.thumbnail.processors import dynamic_import
+from sorl.thumbnail import defaults
+
+
+def get_thumbnail_setting(setting, override=None):
+    &quot;&quot;&quot;
+    Get a thumbnail setting from Django settings module, falling back to the
+    default.
+
+    If override is not None, it will be used instead of the setting.
+    &quot;&quot;&quot;
+    if override is not None:
+        return override
+    if hasattr(settings, 'THUMBNAIL_%s' % setting):
+        return getattr(settings, 'THUMBNAIL_%s' % setting)
+    else:
+        return getattr(defaults, setting)
+
+
+def build_thumbnail_name(source_name, size, options=None,
+                         quality=None, basedir=None, subdir=None, prefix=None,
+                         extension=None):
+    quality = get_thumbnail_setting('QUALITY', quality)
+    basedir = get_thumbnail_setting('BASEDIR', basedir)
+    subdir = get_thumbnail_setting('SUBDIR', subdir)
+    prefix = get_thumbnail_setting('PREFIX', prefix)
+    extension = get_thumbnail_setting('EXTENSION', extension)
+    path, filename = os.path.split(source_name)
+    basename, ext = os.path.splitext(filename)
+    name = '%s%s' % (basename, ext.replace(os.extsep, '_'))
+    size = '%sx%s' % tuple(size)
+
+    # Handle old list format for opts.
+    options = options or {}
+    if isinstance(options, (list, tuple)):
+        options = dict([(opt, None) for opt in options])
+
+    opts = options.items()
+    opts.sort()   # options are sorted so the filename is consistent
+    opts = ['%s_' % (v is not None and '%s-%s' % (k, v) or k)
+            for k, v in opts]
+    opts = ''.join(opts)
+    extension = extension and '.%s' % extension
+    thumbnail_filename = '%s%s_%s_%sq%s%s' % (prefix, name, size, opts,
+                                              quality, extension)
+    return os.path.join(basedir, path, subdir, thumbnail_filename)
 
 
 class DjangoThumbnail(Thumbnail):
     def __init__(self, relative_source, requested_size, opts=None,
-                 quality=None, basedir=None, subdir=None, prefix=None):
+                 quality=None, basedir=None, subdir=None, prefix=None,
+                 relative_dest=None, processors=None, extension=None):
+        relative_source = force_unicode(relative_source)
         # Set the absolute filename for the source file
         source = self._absolute_path(relative_source)
 
         quality = get_thumbnail_setting('QUALITY', quality)
-        imagemagick_path = get_thumbnail_setting('CONVERT')
+        convert_path = get_thumbnail_setting('CONVERT')
         wvps_path = get_thumbnail_setting('WVPS')
+        if processors is None:
+            processors = dynamic_import(get_thumbnail_setting('PROCESSORS'))
 
         # Call super().__init__ now to set the opts attribute. generate() won't
         # get called because we are not setting the dest attribute yet.
         super(DjangoThumbnail, self).__init__(source, requested_size,
-            opts=opts, quality=quality, imagemagick_path=imagemagick_path,
-            wvps_path=wvps_path)
-      
+            opts=opts, quality=quality, convert_path=convert_path,
+            wvps_path=wvps_path, processors=processors)
+
         # Get the relative filename for the thumbnail image, then set the
         # destination filename
-        relative_thumbnail = \
-           self._get_relative_thumbnail(relative_source, basedir=basedir,
-                                        subdir=subdir, prefix=prefix)
-        self.dest = self._absolute_path(relative_thumbnail)
-        
+        if relative_dest is None:
+            relative_dest = \
+               self._get_relative_thumbnail(relative_source, basedir=basedir,
+                                            subdir=subdir, prefix=prefix,
+                                            extension=extension)
+        filelike = not isinstance(relative_dest, basestring)
+        if filelike:
+            self.dest = relative_dest
+        else: 
+            self.dest = self._absolute_path(relative_dest)
+
         # Call generate now that the dest attribute has been set
         self.generate()
 
         # Set the relative &amp; absolute url to the thumbnail
-        self.relative_url = \
-            iri_to_uri('/'.join(relative_thumbnail.split(os.sep)))
-        self.absolute_url = '%s%s' % (settings.MEDIA_URL, self.relative_url)
-    
-    def _get_relative_thumbnail(self, relative_source, 
-                                basedir=None, subdir=None, prefix=None):
+        if not filelike:
+            self.relative_url = \
+                iri_to_uri('/'.join(relative_dest.split(os.sep)))
+            self.absolute_url = '%s%s' % (settings.MEDIA_URL,
+                                          self.relative_url)
+
+    def _get_relative_thumbnail(self, relative_source,
+                                basedir=None, subdir=None, prefix=None,
+                                extension=None):
         &quot;&quot;&quot;
         Returns the thumbnail filename including relative path.
         &quot;&quot;&quot;
-        basedir = get_thumbnail_setting('BASEDIR', basedir)
-        subdir = get_thumbnail_setting('SUBDIR', subdir)
-        prefix = get_thumbnail_setting('PREFIX', prefix)
-        path, filename = os.path.split(relative_source)
-        basename, ext = os.path.splitext(filename)
-        name = '%s%s' % (basename, ext.replace(&quot;.&quot;, &quot;_&quot;))
-        size = '%sx%s' % self.requested_size
-        opts_list = self.opts_list[:]
-        opts_list.append('')
-        opts = '_'.join(opts_list)
-        thumbnail_filename = '%s%s_%s_%sq%s.jpg' % (prefix, name, size,
-                                                    opts, self.quality)
-        return os.path.join(basedir, path, subdir, thumbnail_filename)
-    
+        return build_thumbnail_name(relative_source, self.requested_size,
+                                    self.opts, self.quality, basedir, subdir,
+                                    prefix, extension)
+
     def _absolute_path(self, filename):
         absolute_filename = os.path.join(settings.MEDIA_ROOT, filename)
         return absolute_filename.encode(settings.FILE_CHARSET)</diff>
      <filename>lib/sorl/thumbnail/main.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,14 +2,14 @@ import re
 import math
 from django.template import Library, Node, Variable, VariableDoesNotExist, TemplateSyntaxError
 from django.conf import settings
-from sorl.thumbnail.base import VALID_OPTIONS
-from sorl.thumbnail.main import DjangoThumbnail
-from sorl.thumbnail.utils import get_thumbnail_setting
+from django.utils.encoding import force_unicode
+from sorl.thumbnail.main import DjangoThumbnail, get_thumbnail_setting
+from sorl.thumbnail.processors import dynamic_import, get_valid_options
+from sorl.thumbnail.utils import split_args
 
 register = Library()
 
 size_pat = re.compile(r'(\d+)x(\d+)$')
-quality_pat = re.compile(r'quality=([1-9]\d?|100)$')
 
 filesize_formats = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
 filesize_long_formats = {
@@ -17,70 +17,99 @@ filesize_long_formats = {
     'E': 'exa', 'Z': 'zetta', 'Y': 'yotta'
 }
 
+try:
+    PROCESSORS = dynamic_import(get_thumbnail_setting('PROCESSORS'))
+    VALID_OPTIONS = get_valid_options(PROCESSORS)
+except:
+    if get_thumbnail_setting('DEBUG'):
+        raise
+    else:
+        PROCESSORS = []
+        VALID_OPTIONS = []
+TAG_SETTINGS = ['quality'] 
 
 class ThumbnailNode(Node):
     def __init__(self, source_var, size_var, opts=None,
                  context_name=None, **kwargs):
-        self.source_var = Variable(source_var)
-        m = size_pat.match(size_var)
-        if m:
-            self.requested_size = (int(m.group(1)), int(m.group(2)))
-        else:
-            self.size_var = Variable(size_var)
-            self.requested_size = None
+        self.source_var = source_var
+        self.size_var = size_var
         self.opts = opts
         self.context_name = context_name
         self.kwargs = kwargs
-    
+
     def render(self, context):
+        # Note that this isn't a global constant because we need to change the
+        # value for tests.
         DEBUG = get_thumbnail_setting('DEBUG')
-        # Resolve variables
         try:
+            # A file object will be allowed in DjangoThumbnail class
             relative_source = self.source_var.resolve(context)
-            size = self.requested_size or self.size_var.resolve(context)
         except VariableDoesNotExist:
             if DEBUG:
-                raise VariableDoesNotExist(&quot;Variable %s does not exist.&quot; %
-                                           self.source_var)
-            return ''
-        # Check size
-        try:
-            size = tuple([int(v) for v in size])
-        except (TypeError, ValueError):
-            size = ()
-        if len(size) != 2:
-            if DEBUG:
-                raise TemplateSyntaxError(&quot;Variable %s found but was not a&quot;
-                    &quot;valid size&quot; % self.size_var)
-            return ''
-        # Get thumbnail class
+                raise VariableDoesNotExist(&quot;Variable '%s' does not exist.&quot; %
+                        self.source_var)
+            else:
+                relative_source = None
         try:
-            thumbnail = DjangoThumbnail(relative_source, size, opts=self.opts,
-                                        **self.kwargs)
-        except:
+            requested_size = self.size_var.resolve(context)
+        except VariableDoesNotExist:
             if DEBUG:
-                raise
-            return ''
+                raise TemplateSyntaxError(&quot;Size argument '%s' is not a&quot;
+                        &quot; valid size nor a valid variable.&quot; % self.size_var)
+            else:
+                requested_size = None
+        # Size variable can be either a tuple/list of two integers or a valid
+        # string, only the string is checked.
+        else:
+            if isinstance(requested_size, basestring):
+                m = size_pat.match(requested_size)
+                if m:
+                    requested_size = (int(m.group(1)), int(m.group(2)))
+                elif DEBUG:
+                    raise TemplateSyntaxError(&quot;Variable '%s' was resolved but &quot;
+                            &quot;'%s' is not a valid size.&quot; %
+                            (self.size_var, requested_size))
+                else:
+                    requested_size = None
+        if relative_source is None or requested_size is None:
+            thumbnail = ''
+        else:
+            try:
+                kwargs = {}
+                for key, value in self.kwargs.items():
+                    kwargs[key] = value.resolve(context)
+                opts = dict([(k, v and v.resolve(context))
+                             for k, v in self.opts.items()])
+                thumbnail = DjangoThumbnail(relative_source, requested_size,
+                                opts=opts, processors=PROCESSORS, **kwargs)
+            except:
+                if DEBUG:
+                    raise
+                else:
+                    thumbnail = ''
         # Return the thumbnail class, or put it on the context
         if self.context_name is None:
             return thumbnail
-        if thumbnail:
-            context[self.context_name] = thumbnail
+        # We need to get here so we don't have old values in the context
+        # variable.
+        context[self.context_name] = thumbnail
         return ''
 
 
 def thumbnail(parser, token):
     &quot;&quot;&quot;
+    Creates a thumbnail of for an ImageField.
+
     To just output the absolute url to the thumbnail::
 
         {% thumbnail image 80x80 %}
 
     After the image path and dimensions, you can put any options::
 
-        {% thumbnail image 80x80 quality=95,crop %}
+        {% thumbnail image 80x80 quality=95 crop %}
 
     To put the DjangoThumbnail class on the context instead of just rendering
-    the absolute url, finish the tag with &quot;as [context_var_name]&quot;::
+    the absolute url, finish the tag with ``as [context_var_name]``::
 
         {% thumbnail image 80x80 as thumb %}
         {{ thumb.width }} x {{ thumb.height }}
@@ -88,50 +117,54 @@ def thumbnail(parser, token):
     args = token.split_contents()
     tag = args[0]
     # Check to see if we're setting to a context variable.
-    if len(args) in (5, 6) and args[-2] == 'as':
+    if len(args) &gt; 4 and args[-2] == 'as':
         context_name = args[-1]
         args = args[:-2]
     else:
         context_name = None
 
-    if len(args) not in (3, 4):
+    if len(args) &lt; 3:
         raise TemplateSyntaxError(&quot;Invalid syntax. Expected &quot;
-            &quot;'{%% %s source size [options] %%}' or &quot;
-            &quot;'{%% %s source size [options] as variable %%}'&quot; % (tag, tag)) 
+            &quot;'{%% %s source size [option1 option2 ...] %%}' or &quot;
+            &quot;'{%% %s source size [option1 option2 ...] as variable %%}'&quot; %
+            (tag, tag))
 
     # Get the source image path and requested size.
-    source_var = args[1]
-    size_var = args[2]
+    source_var = parser.compile_filter(args[1])
+    # Since we changed to allow filters we have to make an exception for our syntax
+    m = size_pat.match(args[2])
+    if m:
+        args[2] = '&quot;%s&quot;' % args[2]
+    size_var = parser.compile_filter(args[2])
 
     # Get the options.
-    if len(args) == 4:
-        args_list = args[3].split(',')
-    else:
-        args_list = []
+    args_list = split_args(args[3:]).items()
 
     # Check the options.
-    opts = []
+    opts = {}
     kwargs = {} # key,values here override settings and defaults
-    for arg in args_list:
+
+    for arg, value in args_list:
+        value = value and parser.compile_filter(value)
+        if arg in TAG_SETTINGS and value is not None:
+            kwargs[str(arg)] = value
+            continue
         if arg in VALID_OPTIONS:
-            opts.append(arg)
+            opts[arg] = value
         else:
-            m = quality_pat.match(arg)
-            if not m:
-                raise TemplateSyntaxError(
-                    &quot;'%s' tag received a bad argument: '%s'&quot; % (tag, arg))
-            kwargs['quality'] = int(m.group(1))
+            raise TemplateSyntaxError(&quot;'%s' tag received a bad argument: &quot;
+                                      &quot;'%s'&quot; % (tag, arg))
     return ThumbnailNode(source_var, size_var, opts=opts,
-                         context_name=context_name, **kwargs)
+            context_name=context_name, **kwargs)
 
 
 def filesize(bytes, format='auto1024'):
     &quot;&quot;&quot;
     Returns the number of bytes in either the nearest unit or a specific unit
     (depending on the chosen format method).
-    
+
     Acceptable formats are:
-    
+
     auto1024, auto1000
       convert to the nearest unit, appending the abbreviated unit name to the
       string (e.g. '2 KiB' or '2 kB').
@@ -146,7 +179,7 @@ def filesize(bytes, format='auto1024'):
 
     The auto1024 and auto1000 formats return a string, appending the correct
     unit to the value. All other formats return the floating point value.
-    
+
     If an invalid format is specified, the bytes are returned unchanged.
     &quot;&quot;&quot;
     format_len = len(format)
@@ -198,7 +231,7 @@ def filesize(bytes, format='auto1024'):
         else:
             unit = '%s%s' % (base == 1024 and unit.upper() or unit,
                              base == 1024 and 'iB' or 'B')
-    
+
         return '%s %s' % (bytes, unit)
 
     if bytes == 0:
@@ -207,7 +240,7 @@ def filesize(bytes, format='auto1024'):
     # Exact multiple of 1000
     if format_len == 2:
         return bytes / (1000.0**base)
-    # Exact multiple of 1024 
+    # Exact multiple of 1024
     elif format_len == 3:
         bytes = bytes &gt;&gt; (10*(base-1))
         return bytes / 1024.0</diff>
      <filename>lib/sorl/thumbnail/templatetags/thumbnail.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,11 +1,11 @@
 # For these tests to run successfully, two conditions must be met:
 # 1. MEDIA_URL and MEDIA_ROOT must be set in settings
-# 2. The user running the tests must have read/write access to MEDIA_ROOT 
+# 2. The user running the tests must have read/write access to MEDIA_ROOT
 
 # Unit tests:
 from sorl.thumbnail.tests.classes import ThumbnailTest, DjangoThumbnailTest
 from sorl.thumbnail.tests.templatetags import ThumbnailTagTest
-
+from sorl.thumbnail.tests.fields import FieldTest, ImageWithThumbnailsFieldTest, ThumbnailFieldTest
 # Doc tests:
 from sorl.thumbnail.tests.utils import utils_tests
 from sorl.thumbnail.tests.templatetags import filesize_tests</diff>
      <filename>lib/sorl/thumbnail/tests/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,23 +2,33 @@ import unittest
 import os
 from PIL import Image
 from django.conf import settings
-from sorl.thumbnail.base import Thumbnail 
-from sorl.thumbnail.utils import DEFAULT_THUMBNAIL_SETTINGS
+from sorl.thumbnail.base import Thumbnail
 
 try:
     set
 except NameError:
     from sets import Set as set     # For Python 2.3
 
+def get_default_settings():
+    from sorl.thumbnail import defaults
+    def_settings = {}
+    for key in dir(defaults):
+        if key == key.upper() and key not in ['WVPS', 'CONVERT']:
+            def_settings[key] = getattr(defaults, key)
+    return def_settings
+
+
+DEFAULT_THUMBNAIL_SETTINGS = get_default_settings()
 RELATIVE_PIC_NAME = &quot;sorl-thumbnail-test_source.jpg&quot;
 PIC_NAME = os.path.join(settings.MEDIA_ROOT, RELATIVE_PIC_NAME)
 THUMB_NAME = os.path.join(settings.MEDIA_ROOT, &quot;sorl-thumbnail-test_%02d.jpg&quot;)
 PIC_SIZE = (800, 600)
 
 
+
 class ChangeSettings:
     def __init__(self):
-        self.default_settings = DEFAULT_THUMBNAIL_SETTINGS.copy() 
+        self.default_settings = DEFAULT_THUMBNAIL_SETTINGS.copy()
 
     def change(self, override=None):
         if override is not None:
@@ -31,7 +41,7 @@ class ChangeSettings:
             if hasattr(settings, settings_s) or \
                default != DEFAULT_THUMBNAIL_SETTINGS[setting]:
                 setattr(settings, settings_s, default)
-    
+
     def revert(self):
         for setting in self.default_settings:
             settings_s = 'THUMBNAIL_%s' % setting
@@ -52,7 +62,7 @@ class BaseTest(unittest.TestCase):
         self.change_settings.change()
 
     def verify_thumbnail(self, expected_size, thumbnail=None,
-                         expected_filename=None):
+                         expected_filename=None, expected_mode=None):
         assert thumbnail is not None or expected_filename is not None, \
             'verify_thumbnail should be passed at least a thumbnail or an' \
             'expected filename.'
@@ -64,22 +74,33 @@ class BaseTest(unittest.TestCase):
         else:
             thumb_name = expected_filename
 
-        # Verify that the thumbnail file exists
-        self.assert_(os.path.isfile(thumb_name), 'Thumbnail file not found')
+        if isinstance(thumb_name, basestring):
+            # Verify that the thumbnail file exists
+            self.assert_(os.path.isfile(thumb_name),
+                         'Thumbnail file not found')
 
-        # Remember to delete the file
-        self.images_to_delete.add(thumb_name)
+            # Remember to delete the file
+            self.images_to_delete.add(thumb_name)
 
-        # If we got an expected_filename, check that it is right
-        if expected_filename is not None and thumbnail is not None:
-            self.assertEqual(thumbnail.dest, expected_filename)
+            # If we got an expected_filename, check that it is right
+            if expected_filename is not None and thumbnail is not None:
+                self.assertEqual(thumbnail.dest, expected_filename)
+
+        image = Image.open(thumb_name)
 
         # Verify the thumbnail has the expected dimensions
-        self.assertEqual(Image.open(thumb_name).size, expected_size)
+        self.assertEqual(image.size, expected_size)
+
+        if expected_mode is not None:
+            self.assertEqual(image.mode, expected_mode)
 
     def tearDown(self):
         # Remove all the files that have been created
         for image in self.images_to_delete:
-            os.remove(image)
+            try:
+                os.remove(image)
+            except:
+                pass
         # Change settings back to original
         self.change_settings.revert()
+</diff>
      <filename>lib/sorl/thumbnail/tests/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,10 +1,15 @@
+#! -*- coding: utf-8 -*-
 import unittest
 import os
 import time
+from StringIO import StringIO
+
 from PIL import Image
 from django.conf import settings
-from sorl.thumbnail.base import Thumbnail, VALID_OPTIONS 
-from sorl.thumbnail.main import DjangoThumbnail
+
+from sorl.thumbnail.base import Thumbnail
+from sorl.thumbnail.main import DjangoThumbnail, get_thumbnail_setting
+from sorl.thumbnail.processors import dynamic_import, get_valid_options
 from sorl.thumbnail.tests.base import BaseTest, RELATIVE_PIC_NAME, PIC_NAME, THUMB_NAME, PIC_SIZE
 
 
@@ -39,13 +44,33 @@ class ThumbnailTest(BaseTest):
         thumb = Thumbnail(source=PIC_NAME, dest=thumb_name,
                           requested_size=thumb_size)
         self.assertEqual(os.path.getmtime(thumb_name), thumb_mtime)
-        
+
         # Recreate the source image, then see if a new thumb is generated
         Image.new('RGB', PIC_SIZE).save(PIC_NAME, 'JPEG')
         thumb = Thumbnail(source=PIC_NAME, dest=thumb_name,
                           requested_size=thumb_size)
         self.assertNotEqual(os.path.getmtime(thumb_name), thumb_mtime)
 
+    def testFilelikeDest(self):
+        # Thumbnail
+        filelike_dest = StringIO()
+        thumb = Thumbnail(source=PIC_NAME, dest=filelike_dest,
+                          requested_size=(240, 240))
+        self.verify_thumbnail((240, 180), thumb)
+
+    def testRGBA(self):
+        # RGBA image
+        rgba_pic_name = os.path.join(settings.MEDIA_ROOT,
+                                     'sorl-thumbnail-test_rgba_source.png')
+        Image.new('RGBA', PIC_SIZE).save(rgba_pic_name)
+        self.images_to_delete.add(rgba_pic_name)
+        # Create thumb and verify it's still RGBA
+        rgba_thumb_name = os.path.join(settings.MEDIA_ROOT,
+                                       'sorl-thumbnail-test_rgba_dest.png')
+        thumb = Thumbnail(source=rgba_pic_name, dest=rgba_thumb_name,
+                          requested_size=(240, 240))
+        self.verify_thumbnail((240, 180), thumb, expected_mode='RGBA')
+
 
 class DjangoThumbnailTest(BaseTest):
     def setUp(self):
@@ -79,10 +104,13 @@ class DjangoThumbnailTest(BaseTest):
         self.verify_thumbnail((240, 120), thumb, expected_filename=expected)
 
         # All options on
+        processors = dynamic_import(get_thumbnail_setting('PROCESSORS'))
+        valid_options = get_valid_options(processors)
+
         thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
-                                requested_size=(240, 120), opts=VALID_OPTIONS)
-        expected = os.path.join(settings.MEDIA_ROOT, basename)
-        expected += '_240x120_crop_autocrop_upscale_bw_detail_sharpen_q85.jpg'
+                                requested_size=(240, 120), opts=valid_options)
+        expected = (os.path.join(settings.MEDIA_ROOT, basename) + '_240x120_'
+                    'autocrop_bw_crop_detail_max_sharpen_upscale_q85.jpg')
         self.verify_thumbnail((240, 120), thumb, expected_filename=expected)
 
         # Different basedir
@@ -106,11 +134,45 @@ class DjangoThumbnailTest(BaseTest):
         self.change_settings.change({'SUBDIR': '', 'PREFIX': 'prefix-'})
         thumb = DjangoThumbnail(relative_source=self.pic_subdir,
                                 requested_size=(240, 120))
-        expected = os.path.join(self.sub_dir, 'prefix-'+basename)
+        expected = os.path.join(self.sub_dir, 'prefix-' + basename)
         expected += '_240x120_q85.jpg'
         self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
 
+    def testAlternateExtension(self):
+        basename = RELATIVE_PIC_NAME.replace('.', '_')
+        # Control JPG
+        thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+                                requested_size=(240, 120))
+        expected = os.path.join(settings.MEDIA_ROOT, basename)
+        expected += '_240x120_q85.jpg'
+        expected_jpg = expected
+        self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+        # Test PNG
+        thumb = DjangoThumbnail(relative_source=RELATIVE_PIC_NAME,
+                                requested_size=(240, 120), extension='png')
+        expected = os.path.join(settings.MEDIA_ROOT, basename)
+        expected += '_240x120_q85.png'
+        self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+        # Compare the file size to make sure it's not just saving as a JPG with
+        # a different extension.
+        self.assertNotEqual(os.path.getsize(expected_jpg),
+                            os.path.getsize(expected))
+
+    def testUnicodeName(self):
+        unicode_name = 'sorl-thumbnail-&#261;&#380;&#347;&#378;_source.jpg'
+        unicode_path = os.path.join(settings.MEDIA_ROOT, unicode_name)
+        Image.new('RGB', PIC_SIZE).save(unicode_path)
+        self.images_to_delete.add(unicode_path)
+        thumb = DjangoThumbnail(relative_source=unicode_name,
+                                requested_size=(240, 120))
+        base_name = unicode_name.replace('.', '_')
+        expected = os.path.join(settings.MEDIA_ROOT,
+                                base_name + '_240x120_q85.jpg')
+        self.verify_thumbnail((160, 120), thumb, expected_filename=expected)
+
     def tearDown(self):
         super(DjangoThumbnailTest, self).tearDown()
-        os.rmdir(os.path.join(self.sub_dir, 'subdir'))
+        subdir = os.path.join(self.sub_dir, 'subdir')
+        if os.path.exists(subdir):
+            os.rmdir(subdir)
         os.rmdir(self.sub_dir)</diff>
      <filename>lib/sorl/thumbnail/tests/classes.py</filename>
    </modified>
    <modified>
      <diff>@@ -4,6 +4,7 @@ import time
 from PIL import Image
 from django.conf import settings
 from django.template import Template, Context, TemplateSyntaxError
+from sorl.thumbnail.base import ThumbnailException
 from sorl.thumbnail.tests.classes import BaseTest, RELATIVE_PIC_NAME
 
 
@@ -13,7 +14,10 @@ class ThumbnailTagTest(BaseTest):
             'source': RELATIVE_PIC_NAME,
             'invalid_source': 'not%s' % RELATIVE_PIC_NAME,
             'size': (90, 100),
-            'invalid_size': (90, 'fish')})
+            'invalid_size': (90, 'fish'),
+            'strsize': '80x90',
+            'invalid_strsize': ('1notasize2'),
+            'invalid_q': 'notanumber'})
         source = '{% load thumbnail %}' + source
         return Template(source).render(context)
 
@@ -25,28 +29,38 @@ class ThumbnailTagTest(BaseTest):
         self.assertRaises(TemplateSyntaxError, self.render_template, src)
         src = '{% thumbnail source %}'
         self.assertRaises(TemplateSyntaxError, self.render_template, src)
-        src = '{% thumbnail source 80x80 Xas variable %}'
-        self.assertRaises(TemplateSyntaxError, self.render_template, src)
-        src = '{% thumbnail source 80x80 as variable X %}'
+        src = '{% thumbnail source 80x80 as variable crop %}'
         self.assertRaises(TemplateSyntaxError, self.render_template, src)
 
         # Invalid option
         src = '{% thumbnail source 240x200 invalid %}'
         self.assertRaises(TemplateSyntaxError, self.render_template, src)
 
-        # Invalid quality
-        src = '{% thumbnail source 240x200 quality=a %}'
+        # Old comma separated options format can only have an = for quality
+        src = '{% thumbnail source 80x80 crop=1,quality=1 %}'
         self.assertRaises(TemplateSyntaxError, self.render_template, src)
 
+        # Invalid quality
+        src_invalid = '{% thumbnail source 240x200 quality=invalid_q %}'
+        src_missing = '{% thumbnail source 240x200 quality=missing_q %}'
+        # ...with THUMBNAIL_DEBUG = False
+        self.assertEqual(self.render_template(src_invalid), '')
+        self.assertEqual(self.render_template(src_missing), '')
+        # ...and with THUMBNAIL_DEBUG = True
+        self.change_settings.change({'DEBUG': True})
+        self.assertRaises(TypeError, self.render_template, src_invalid)
+        self.assertRaises(TypeError, self.render_template, src_missing)
+
         # Invalid source
         src = '{% thumbnail invalid_source 80x80 %}'
         src_on_context = '{% thumbnail invalid_source 80x80 as thumb %}'
         # ...with THUMBNAIL_DEBUG = False
+        self.change_settings.change({'DEBUG': False})
         self.assertEqual(self.render_template(src), '')
         # ...and with THUMBNAIL_DEBUG = True
         self.change_settings.change({'DEBUG': True})
-        self.assertRaises(TemplateSyntaxError, self.render_template, src)
-        self.assertRaises(TemplateSyntaxError, self.render_template,
+        self.assertRaises(ThumbnailException, self.render_template, src)
+        self.assertRaises(ThumbnailException, self.render_template,
                           src_on_context)
 
         # Non-existant source
@@ -57,15 +71,23 @@ class ThumbnailTagTest(BaseTest):
         self.assertEqual(self.render_template(src), '')
         # ...and with THUMBNAIL_DEBUG = True
         self.change_settings.change({'DEBUG': True})
-        self.assertRaises(TemplateSyntaxError, self.render_template, src)
+        self.assertRaises(ThumbnailException, self.render_template, src)
 
-        # Invalid size
+        # Invalid size as a tuple:
         src = '{% thumbnail source invalid_size %}'
         # ...with THUMBNAIL_DEBUG = False
         self.change_settings.change({'DEBUG': False})
         self.assertEqual(self.render_template(src), '')
         # ...and THUMBNAIL_DEBUG = True
         self.change_settings.change({'DEBUG': True})
+        self.assertRaises(TypeError, self.render_template, src)
+        # Invalid size as a string:
+        src = '{% thumbnail source invalid_strsize %}'
+        # ...with THUMBNAIL_DEBUG = False
+        self.change_settings.change({'DEBUG': False})
+        self.assertEqual(self.render_template(src), '')
+        # ...and THUMBNAIL_DEBUG = True
+        self.change_settings.change({'DEBUG': True})
         self.assertRaises(TemplateSyntaxError, self.render_template, src)
 
         # Non-existant size
@@ -92,6 +114,7 @@ class ThumbnailTagTest(BaseTest):
         self.assertEqual(output, 'src=&quot;%s&quot;' % expected_url)
 
         # Size from context variable
+        # as a tuple:
         output = self.render_template('src=&quot;'
             '{% thumbnail source size %}&quot;')
         expected = '%s_90x100_q85.jpg' % expected_base
@@ -99,6 +122,14 @@ class ThumbnailTagTest(BaseTest):
         self.verify_thumbnail((90, 67), expected_filename=expected_fn)
         expected_url = ''.join((settings.MEDIA_URL, expected))
         self.assertEqual(output, 'src=&quot;%s&quot;' % expected_url)
+        # as a string:
+        output = self.render_template('src=&quot;'
+            '{% thumbnail source strsize %}&quot;')
+        expected = '%s_80x90_q85.jpg' % expected_base
+        expected_fn = os.path.join(settings.MEDIA_ROOT, expected)
+        self.verify_thumbnail((80, 60), expected_filename=expected_fn)
+        expected_url = ''.join((settings.MEDIA_URL, expected))
+        self.assertEqual(output, 'src=&quot;%s&quot;' % expected_url)
 
         # On context
         output = self.render_template('height:'
@@ -107,9 +138,8 @@ class ThumbnailTagTest(BaseTest):
 
         # With options and quality
         output = self.render_template('src=&quot;'
-            '{% thumbnail source 240x240 sharpen,crop,quality=95 %}&quot;')
-        # Note that the order of opts comes from VALID_OPTIONS to ensure a
-        # consistent filename.
+            '{% thumbnail source 240x240 sharpen crop quality=95 %}&quot;')
+        # Note that the opts are sorted to ensure a consistent filename.
         expected = '%s_240x240_crop_sharpen_q95.jpg' % expected_base
         expected_fn = os.path.join(settings.MEDIA_ROOT, expected)
         self.verify_thumbnail((240, 240), expected_filename=expected_fn)
@@ -119,6 +149,12 @@ class ThumbnailTagTest(BaseTest):
         # With option and quality on context (also using its unicode method to
         # display the url)
         output = self.render_template(
+            '{% thumbnail source 240x240 sharpen crop quality=95 as thumb %}'
+            'width:{{ thumb.width }}, url:{{ thumb }}')
+        self.assertEqual(output, 'width:240, url:%s' % expected_url)
+
+        # Old comma separated format for options is still supported.
+        output = self.render_template(
             '{% thumbnail source 240x240 sharpen,crop,quality=95 as thumb %}'
             'width:{{ thumb.width }}, url:{{ thumb }}')
         self.assertEqual(output, 'width:240, url:%s' % expected_url)
@@ -169,7 +205,7 @@ filesize_tests = &quot;&quot;&quot;
 
 # Test all 'auto*long' output:
 &gt;&gt;&gt; for i in range(1,10):
-...     print '%s, %s' % (filesize(1024**i, 'auto1024long'), 
+...     print '%s, %s' % (filesize(1024**i, 'auto1024long'),
 ...                       filesize(1000**i, 'auto1000long'))
 1 kibibyte, 1 kilobyte
 1 mebibyte, 1 megabyte</diff>
      <filename>lib/sorl/thumbnail/tests/templatetags.py</filename>
    </modified>
    <modified>
      <diff>@@ -21,12 +21,12 @@ utils_tests = &quot;&quot;&quot;
 # Set up test images
 #===============================================================================
 
+&gt;&gt;&gt; make_image('test-thumbnail-utils/subdir/test_jpg_110x110_q85.jpg')
 &gt;&gt;&gt; make_image('test-thumbnail-utils/test_jpg_80x80_q85.jpg')
 &gt;&gt;&gt; make_image('test-thumbnail-utils/test_jpg_80x80_q95.jpg')
 &gt;&gt;&gt; make_image('test-thumbnail-utils/another_test_jpg_80x80_q85.jpg')
 &gt;&gt;&gt; make_image('test-thumbnail-utils/test_with_opts_jpg_80x80_crop_bw_q85.jpg')
 &gt;&gt;&gt; make_image('test-thumbnail-basedir/test-thumbnail-utils/test_jpg_100x100_q85.jpg')
-&gt;&gt;&gt; make_image('test-thumbnail-utils/subdir/test_jpg_110x110_q85.jpg')
 &gt;&gt;&gt; make_image('test-thumbnail-utils/prefix-test_jpg_120x120_q85.jpg')
 
 #===============================================================================
@@ -38,7 +38,7 @@ utils_tests = &quot;&quot;&quot;
 &gt;&gt;&gt; thumbs = all_thumbnails(thumb_dir)
 &gt;&gt;&gt; k = thumbs.keys()
 &gt;&gt;&gt; k.sort()
-&gt;&gt;&gt; k
+&gt;&gt;&gt; [consistent_slash(path) for path in k]
 ['another_test.jpg', 'prefix-test.jpg', 'subdir/test.jpg', 'test.jpg', 'test_with_opts.jpg']
 
 # Find all thumbs, no recurse
@@ -52,34 +52,42 @@ utils_tests = &quot;&quot;&quot;
 # thumbnails_for_file()
 #===============================================================================
 
+&gt;&gt;&gt; output = []
 &gt;&gt;&gt; for thumb in thumbs['test.jpg']:
-...     thumb['rel_fn'] = thumb['filename'][len(settings.MEDIA_ROOT)+1:]
-...     print '%(x)sx%(y)s %(quality)s %(rel_fn)s' % thumb
-80x80 85 test-thumbnail-utils/test_jpg_80x80_q85.jpg
-80x80 95 test-thumbnail-utils/test_jpg_80x80_q95.jpg
+...     thumb['rel_fn'] = strip_media_root(thumb['filename'])
+...     output.append('%(x)sx%(y)s %(quality)s %(rel_fn)s' % thumb)
+&gt;&gt;&gt; output.sort()
+&gt;&gt;&gt; output
+['80x80 85 test-thumbnail-utils/test_jpg_80x80_q85.jpg', '80x80 95 test-thumbnail-utils/test_jpg_80x80_q95.jpg']
 
 # Thumbnails for file
+&gt;&gt;&gt; output = []
 &gt;&gt;&gt; for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
-...    print thumb['filename'][len(settings.MEDIA_ROOT)+1:]
-test-thumbnail-utils/test_jpg_80x80_q85.jpg
-test-thumbnail-utils/test_jpg_80x80_q95.jpg
+...    output.append(strip_media_root(thumb['filename']))
+&gt;&gt;&gt; output.sort()
+&gt;&gt;&gt; output
+['test-thumbnail-utils/test_jpg_80x80_q85.jpg', 'test-thumbnail-utils/test_jpg_80x80_q95.jpg']
+
+# Thumbnails for file - shouldn't choke on non-existant file
+&gt;&gt;&gt; thumbnails_for_file('test-thumbnail-utils/non-existant.jpg')
+[]
 
 # Thumbnails for file, with basedir setting
 &gt;&gt;&gt; change_settings.change({'BASEDIR': 'test-thumbnail-basedir'})
 &gt;&gt;&gt; for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
-...    print thumb['filename'][len(settings.MEDIA_ROOT)+1:]
+...    print strip_media_root(thumb['filename'])
 test-thumbnail-basedir/test-thumbnail-utils/test_jpg_100x100_q85.jpg
 
 # Thumbnails for file, with subdir setting
 &gt;&gt;&gt; change_settings.change({'SUBDIR': 'subdir', 'BASEDIR': ''})
 &gt;&gt;&gt; for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
-...    print thumb['filename'][len(settings.MEDIA_ROOT)+1:]
+...    print strip_media_root(thumb['filename'])
 test-thumbnail-utils/subdir/test_jpg_110x110_q85.jpg
 
 # Thumbnails for file, with prefix setting
 &gt;&gt;&gt; change_settings.change({'PREFIX': 'prefix-', 'SUBDIR': ''})
 &gt;&gt;&gt; for thumb in thumbnails_for_file('test-thumbnail-utils/test.jpg'):
-...    print thumb['filename'][len(settings.MEDIA_ROOT)+1:]
+...    print strip_media_root(thumb['filename'])
 test-thumbnail-utils/prefix-test_jpg_120x120_q85.jpg
 
 #===============================================================================
@@ -90,7 +98,7 @@ test-thumbnail-utils/prefix-test_jpg_120x120_q85.jpg
 &quot;&quot;&quot;
 
 images_to_delete = set()
-dirs_to_delete = set()
+dirs_to_delete = []
 
 def make_image(relative_image):
     absolute_image = os.path.join(settings.MEDIA_ROOT, relative_image)
@@ -104,7 +112,8 @@ def make_dirs(relative_path):
     absolute_path = os.path.join(settings.MEDIA_ROOT, relative_path)
     if os.path.isdir(absolute_path):
         return
-    dirs_to_delete.add(absolute_path)
+    if absolute_path not in dirs_to_delete:
+        dirs_to_delete.append(absolute_path)
     make_dirs(os.path.dirname(relative_path))
     os.mkdir(absolute_path)
 
@@ -112,4 +121,20 @@ def clean_up():
     for image in images_to_delete:
         os.remove(image)
     for path in dirs_to_delete:
-        os.rmdir(path) 
+        os.rmdir(path)
+
+MEDIA_ROOT_LENGTH = len(os.path.normpath(settings.MEDIA_ROOT))
+def strip_media_root(path):
+    path = os.path.normpath(path)
+    # chop off the MEDIA_ROOT and strip any leading os.sep
+    path = path[MEDIA_ROOT_LENGTH:].lstrip(os.sep)
+    return consistent_slash(path)
+
+def consistent_slash(path):
+    &quot;&quot;&quot;
+    Ensure we're always testing against the '/' os separator (otherwise tests
+    fail against Windows).
+    &quot;&quot;&quot;
+    if os.sep != '/':
+        path = path.replace(os.sep, '/')
+    return path</diff>
      <filename>lib/sorl/thumbnail/tests/utils.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,44 +1,26 @@
 import re
 import os
-from django.conf import settings
 
-re_thumbnail_file = re.compile(r'(?P&lt;source_filename&gt;.+)_(?P&lt;x&gt;\d+)x(?P&lt;y&gt;\d+)(?:_(?P&lt;options&gt;\w+))?_q(?P&lt;quality&gt;\d+).jpg$')
 
-DEFAULT_THUMBNAIL_SETTINGS = {
-    'DEBUG': False,
-    'BASEDIR': '',
-    'SUBDIR': '',
-    'PREFIX': '',
-    'QUALITY': 85,
-    'CONVERT': '/usr/bin/convert',
-    'WVPS': '/usr/bin/wvPS',
-}
-
-
-def get_thumbnail_setting(setting, override=None):
-    &quot;&quot;&quot;
-    Get a thumbnail setting from Django settings module, falling back to the
-    default.
-    
-    If override is not None, it will be used instead of the setting.
-    &quot;&quot;&quot;
-    if override is not None:
-        return override
-    if hasattr(settings, 'THUMBNAIL_%s' % setting):
-        return getattr(settings, 'THUMBNAIL_%s' % setting)
-    else:
-        return DEFAULT_THUMBNAIL_SETTINGS[setting]
+re_thumbnail_file = re.compile(r'(?P&lt;source_filename&gt;.+)_(?P&lt;x&gt;\d+)x(?P&lt;y&gt;\d+)(?:_(?P&lt;options&gt;\w+))?_q(?P&lt;quality&gt;\d+)(?:.[^.]+)?$')
+re_new_args = re.compile('(?&lt;!quality)=')
 
 
 def all_thumbnails(path, recursive=True, prefix=None, subdir=None):
     &quot;&quot;&quot;
     Return a dictionary referencing all files which match the thumbnail format.
-    
+
     Each key is a source image filename, relative to path.
     Each value is a list of dictionaries as explained in `thumbnails_for_file`.
     &quot;&quot;&quot;
-    prefix = get_thumbnail_setting('PREFIX', prefix)
-    subdir = get_thumbnail_setting('SUBDIR', subdir)
+    # Fall back to using thumbnail settings. These are local imports so that
+    # there is no requirement of Django to use the utils module.
+    if prefix is None:
+        from sorl.thumbnail.main import get_thumbnail_setting
+        prefix = get_thumbnail_setting('PREFIX')
+    if subdir is None:
+        from sorl.thumbnail.main import get_thumbnail_setting
+        subdir = get_thumbnail_setting('SUBDIR')
     thumbnail_files = {}
     if not path.endswith('/'):
         path = '%s/' % path
@@ -67,7 +49,7 @@ def all_thumbnails(path, recursive=True, prefix=None, subdir=None):
                     source_filename[len(prefix):])
             d['options'] = d['options'] and d['options'].split('_') or []
             if subdir and rel_dir.endswith(subdir):
-                rel_dir = rel_dir[:-len(subdir)] 
+                rel_dir = rel_dir[:-len(subdir)]
             # Corner-case bug: if the filename didn't have an extension but did
             # have an underscore, the last underscore will get converted to a
             # '.'.
@@ -89,15 +71,25 @@ def thumbnails_for_file(relative_source_path, root=None, basedir=None,
 
     The following list explains each key of the dictionary:
 
-      `filename`  -- absolute thumbnail path 
+      `filename`  -- absolute thumbnail path
       `x` and `y` -- the size of the thumbnail
       `options`   -- list of options for this thumbnail
       `quality`   -- quality setting for this thumbnail
     &quot;&quot;&quot;
+    # Fall back to using thumbnail settings. These are local imports so that
+    # there is no requirement of Django to use the utils module.
     if root is None:
+        from django.conf import settings
         root = settings.MEDIA_ROOT
-    basedir = get_thumbnail_setting('BASEDIR', basedir)
-    subdir = get_thumbnail_setting('SUBDIR', subdir)
+    if prefix is None:
+        from sorl.thumbnail.main import get_thumbnail_setting
+        prefix = get_thumbnail_setting('PREFIX')
+    if subdir is None:
+        from sorl.thumbnail.main import get_thumbnail_setting
+        subdir = get_thumbnail_setting('SUBDIR')
+    if basedir is None:
+        from sorl.thumbnail.main import get_thumbnail_setting
+        basedir = get_thumbnail_setting('BASEDIR')
     source_dir, filename = os.path.split(relative_source_path)
     thumbs_path = os.path.join(root, basedir, source_dir, subdir)
     if not os.path.isdir(thumbs_path):
@@ -118,15 +110,22 @@ def delete_thumbnails(relative_source_path, root=None, basedir=None,
 
 
 def _delete_using_thumbs_list(thumbs):
+    deleted = 0
     for thumb_dict in thumbs:
-        os.remove(thumb_dict['filename'])
-    return len(thumbs)
+        filename = thumb_dict['filename']
+        try:
+            os.remove(filename)
+        except:
+            pass
+        else:
+            deleted += 1
+    return deleted
 
 
 def delete_all_thumbnails(path, recursive=True):
     &quot;&quot;&quot;
     Delete all files within a path which match the thumbnails pattern.
-    
+
     By default, matching files from all sub-directories are also removed. To
     only remove from the path directory, set recursive=False.
     &quot;&quot;&quot;
@@ -134,3 +133,25 @@ def delete_all_thumbnails(path, recursive=True):
     for thumbs in all_thumbnails(path, recursive=recursive).values():
         total += _delete_using_thumbs_list(thumbs)
     return total
+
+
+def split_args(args):
+    &quot;&quot;&quot;
+    Split a list of argument strings into a dictionary where each key is an
+    argument name.
+    
+    An argument looks like ``crop``, ``crop=&quot;some option&quot;`` or ``crop=my_var``.
+    Arguments which provide no value get a value of ``None``.
+    &quot;&quot;&quot;
+    if not args:
+        return {}
+    # Handle the old comma separated argument format.
+    if len(args) == 1 and not re_new_args.search(args[0]):
+        args = args[0].split(',')
+    # Separate out the key and value for each argument.
+    args_dict = {}
+    for arg in args:
+        split_arg = arg.split('=', 1)
+        value = len(split_arg) &gt; 1 and split_arg[1] or None
+        args_dict[split_arg[0]] = value
+    return args_dict</diff>
      <filename>lib/sorl/thumbnail/utils.py</filename>
    </modified>
    <modified>
      <diff>@@ -18,7 +18,6 @@ def register(model, tag_descriptor_attr='tags',
     Sets the given model class up for working with tags.
     &quot;&quot;&quot;
     if model in registry:
-        return
         raise AlreadyRegistered(
             _('The model %s has already been registered.') % model.__name__)
     registry.append(model)</diff>
      <filename>lib/tagging/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -3,13 +3,11 @@ A custom Model Field for tagging.
 &quot;&quot;&quot;
 from django.db.models import signals
 from django.db.models.fields import CharField
-from django.dispatch import dispatcher
 from django.utils.translation import ugettext_lazy as _
 
 from tagging import settings
 from tagging.models import Tag
 from tagging.utils import edit_string_for_tags
-from tagging.validators import isTagList
 
 class TagField(CharField):
     &quot;&quot;&quot;
@@ -17,11 +15,10 @@ class TagField(CharField):
     &quot;under the hood&quot;. This exposes a space-separated string of tags, but does
     the splitting/reordering/etc. under the hood.
     &quot;&quot;&quot;
-    def __init__(self, **kwargs):
+    def __init__(self, *args, **kwargs):
         kwargs['max_length'] = kwargs.get('max_length', 255)
         kwargs['blank'] = kwargs.get('blank', True)
-        kwargs['validator_list'] = [isTagList] + kwargs.get('validator_list', [])
-        super(TagField, self).__init__(**kwargs)
+        super(TagField, self).__init__(*args, **kwargs)
 
     def contribute_to_class(self, cls, name):
         super(TagField, self).contribute_to_class(cls, name)
@@ -30,7 +27,7 @@ class TagField(CharField):
         setattr(cls, self.name, self)
 
         # Save tags back to the database post-save
-        dispatcher.connect(self._save, signal=signals.post_save, sender=cls)
+        signals.post_save.connect(self._save, cls, True)
 
     def __get__(self, instance, owner=None):
         &quot;&quot;&quot;
@@ -74,13 +71,13 @@ class TagField(CharField):
             value = value.lower()
         self._set_instance_tag_cache(instance, value)
 
-    def _save(self, signal, sender, instance):
+    def _save(self, **kwargs): #signal, sender, instance):
         &quot;&quot;&quot;
         Save tags back to the database
         &quot;&quot;&quot;
-        tags = self._get_instance_tag_cache(instance)
+        tags = self._get_instance_tag_cache(kwargs['instance'])
         if tags is not None:
-            Tag.objects.update_tags(instance, tags)
+            Tag.objects.update_tags(kwargs['instance'], tags)
 
     def __delete__(self, instance):
         &quot;&quot;&quot;</diff>
      <filename>lib/tagging/fields.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,7 @@
 &quot;&quot;&quot;
 Tagging components for Django's ``newforms`` form library.
 &quot;&quot;&quot;
-from django import newforms as forms
+from django import forms
 from django.utils.translation import ugettext as _
 
 from tagging import settings</diff>
      <filename>lib/tagging/forms.py</filename>
    </modified>
    <modified>
      <diff>@@ -23,7 +23,7 @@ class ModelTagManager(models.Manager):
         return Tag.objects.related_for_model(tags, self.model, *args, **kwargs)
 
     def usage(self, *args, **kwargs):
-        return Tag.objects.usage_for_model(self.model, *arg, **kwargs)
+        return Tag.objects.usage_for_model(self.model, *args, **kwargs)
 
 class ModelTaggedItemManager(models.Manager):
     &quot;&quot;&quot;
@@ -64,5 +64,5 @@ class TagDescriptor(object):
     def __set__(self, instance, value):
         Tag.objects.update_tags(instance, value)
 
-    def __del__(self, instance):
-        Tag.objects.update_tags(instance, None)
\ No newline at end of file
+    def __delete__(self, instance):
+        Tag.objects.update_tags(instance, None)</diff>
      <filename>lib/tagging/managers.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,19 +2,20 @@
 Models and managers for generic tagging.
 &quot;&quot;&quot;
 # Python 2.3 compatibility
-if not hasattr(__builtins__, 'set'):
+try:
+    set
+except NameError:
     from sets import Set as set
 
 from django.contrib.contenttypes import generic
 from django.contrib.contenttypes.models import ContentType
 from django.db import connection, models
-from django.db.models.query import QuerySet, parse_lookup
+from django.db.models.query import QuerySet
 from django.utils.translation import ugettext_lazy as _
 
 from tagging import settings
 from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input
 from tagging.utils import LOGARITHMIC
-from tagging.validators import isTag
 
 qn = connection.ops.quote_name
 
@@ -74,25 +75,11 @@ class TagManager(models.Manager):
         return self.filter(items__content_type__pk=ctype.pk,
                            items__object_id=obj.pk)
 
-    def usage_for_model(self, model, counts=False, min_count=None, filters=None):
+    def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None):
         &quot;&quot;&quot;
-        Obtain a list of tags associated with instances of the given
-        Model class.
-
-        If ``counts`` is True, a ``count`` attribute will be added to
-        each tag, indicating how many times it has been used against
-        the Model class in question.
-
-        If ``min_count`` is given, only tags which have a ``count``
-        greater than or equal to ``min_count`` will be returned.
-        Passing a value for ``min_count`` implies ``counts=True``.
-
-        To limit the tags (and counts, if specified) returned to those
-        used by a subset of the Model's instances, pass a dictionary
-        of field lookups to be applied to the given Model as the
-        ``filters`` argument.
+        Perform the custom SQL query for ``usage_for_model`` and
+        ``usage_for_queryset``.
         &quot;&quot;&quot;
-        if filters is None: filters = {}
         if min_count is not None: counts = True
 
         model_table = qn(model._meta.db_table)
@@ -119,15 +106,7 @@ class TagManager(models.Manager):
             'content_type_id': ContentType.objects.get_for_model(model).pk,
         }
 
-        extra_joins = ''
-        extra_criteria = ''
         min_count_sql = ''
-        params = []
-        if len(filters) &gt; 0:
-            joins, where, params = parse_lookup(filters.items(), model._meta)
-            extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
-                                    for (alias, (table, join_type, condition)) in joins.items()])
-            extra_criteria = 'AND %s' % (' AND '.join(where))
         if min_count is not None:
             min_count_sql = 'HAVING COUNT(%s) &gt;= %%s' % model_pk
             params.append(min_count)
@@ -142,6 +121,55 @@ class TagManager(models.Manager):
             tags.append(t)
         return tags
 
+    def usage_for_model(self, model, counts=False, min_count=None, filters=None):
+        &quot;&quot;&quot;
+        Obtain a list of tags associated with instances of the given
+        Model class.
+
+        If ``counts`` is True, a ``count`` attribute will be added to
+        each tag, indicating how many times it has been used against
+        the Model class in question.
+
+        If ``min_count`` is given, only tags which have a ``count``
+        greater than or equal to ``min_count`` will be returned.
+        Passing a value for ``min_count`` implies ``counts=True``.
+
+        To limit the tags (and counts, if specified) returned to those
+        used by a subset of the Model's instances, pass a dictionary
+        of field lookups to be applied to the given Model as the
+        ``filters`` argument.
+        &quot;&quot;&quot;
+        if filters is None: filters = {}
+
+        queryset = model._default_manager.filter()
+        for f in filters.items():
+            queryset.query.add_filter(f)
+        usage = self.usage_for_queryset(queryset, counts, min_count)
+
+        return usage
+
+    def usage_for_queryset(self, queryset, counts=False, min_count=None):
+        &quot;&quot;&quot;
+        Obtain a list of tags associated with instances of a model
+        contained in the given queryset.
+
+        If ``counts`` is True, a ``count`` attribute will be added to
+        each tag, indicating how many times it has been used against
+        the Model class in question.
+
+        If ``min_count`` is given, only tags which have a ``count``
+        greater than or equal to ``min_count`` will be returned.
+        Passing a value for ``min_count`` implies ``counts=True``.
+        &quot;&quot;&quot;
+
+        extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:])
+        where, params = queryset.query.where.as_sql()
+        if where:
+            extra_criteria = 'AND %s' % where
+        else:
+            extra_criteria = ''
+        return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params)
+
     def related_for_model(self, tags, model, counts=False, min_count=None):
         &quot;&quot;&quot;
         Obtain a list of tags related to a given list of tags - that
@@ -239,7 +267,7 @@ class TaggedItemManager(models.Manager):
           objects we're interested in, then use the ORM's ``__in``
           lookup to return a ``QuerySet``.
 
-          Once the queryset-refactor branch lands in trunk, this can be
+          Now that the queryset-refactor branch is in the trunk, this can be
           tidied up significantly.
     &quot;&quot;&quot;
     def get_by_model(self, queryset_or_model, tags):
@@ -284,6 +312,10 @@ class TaggedItemManager(models.Manager):
         tags = get_tag_list(tags)
         tag_count = len(tags)
         queryset, model = get_queryset_and_model(queryset_or_model)
+
+        if not tag_count:
+            return model._default_manager.none()
+
         model_table = qn(model._meta.db_table)
         # This query selects the ids of all objects which have all the
         # given tags.
@@ -319,6 +351,10 @@ class TaggedItemManager(models.Manager):
         tags = get_tag_list(tags)
         tag_count = len(tags)
         queryset, model = get_queryset_and_model(queryset_or_model)
+
+        if not tag_count:
+            return model._default_manager.none()
+
         model_table = qn(model._meta.db_table)
         # This query selects the ids of all objects which have any of
         # the given tags.
@@ -383,17 +419,23 @@ class TaggedItemManager(models.Manager):
             'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
             'content_type_id': content_type.pk,
             'related_content_type_id': related_content_type.pk,
-            'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
+            # Hardcoding this for now just to get tests working again - this
+            # should now be handled by the query object.
+            'limit_offset': num is not None and 'LIMIT %s' or '',
         }
 
         cursor = connection.cursor()
-        cursor.execute(query, [obj.pk])
+        params = [obj.pk]
+        if num is not None:
+            params.append(num)
+        cursor.execute(query, params)
         object_ids = [row[0] for row in cursor.fetchall()]
         if len(object_ids) &gt; 0:
             # Use in_bulk here instead of an id__in lookup, because id__in would
             # clobber the ordering.
             object_dict = queryset.in_bulk(object_ids)
-            return [object_dict[object_id] for object_id in object_ids]
+            return [object_dict[object_id] for object_id in object_ids \
+                    if object_id in object_dict]
         else:
             return []
 
@@ -405,7 +447,7 @@ class Tag(models.Model):
     &quot;&quot;&quot;
     A tag.
     &quot;&quot;&quot;
-    name = models.CharField(_('name'), max_length=50, unique=True, db_index=True, validator_list=[isTag])
+    name = models.CharField(_('name'), max_length=50, unique=True, db_index=True)
 
     objects = TagManager()
 
@@ -414,9 +456,6 @@ class Tag(models.Model):
         verbose_name = _('tag')
         verbose_name_plural = _('tags')
 
-    class Admin:
-        pass
-
     def __unicode__(self):
         return self.name
 
@@ -437,8 +476,5 @@ class TaggedItem(models.Model):
         verbose_name = _('tagged item')
         verbose_name_plural = _('tagged items')
 
-    class Admin:
-        pass
-
     def __unicode__(self):
         return u'%s [%s]' % (self.object, self.tag)</diff>
      <filename>lib/tagging/models.py</filename>
    </modified>
    <modified>
      <diff>@@ -35,4 +35,4 @@ class Article(models.Model):
         ordering = ['name']
 
 class FormTest(models.Model):
-    tags = TagField()
+    tags = TagField('Test', help_text='Test')</diff>
      <filename>lib/tagging/tests/models.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 r&quot;&quot;&quot;
 &gt;&gt;&gt; import os
-&gt;&gt;&gt; from django import newforms as forms
+&gt;&gt;&gt; from django import forms
 &gt;&gt;&gt; from tagging.forms import TagField
 &gt;&gt;&gt; from tagging import settings
 &gt;&gt;&gt; from tagging.models import Tag, TaggedItem</diff>
      <filename>lib/tagging/tests/tests.py</filename>
    </modified>
    <modified>
      <diff>@@ -10,7 +10,9 @@ from django.utils.encoding import force_unicode
 from django.utils.translation import ugettext as _
 
 # Python 2.3 compatibility
-if not hasattr(__builtins__, 'set'):
+try:
+    set
+except NameError:
     from sets import Set as set
 
 def parse_tag_input(input):</diff>
      <filename>lib/tagging/utils.py</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>lib/sorl/thumbnail/methods.py</filename>
    </removed>
    <removed>
      <filename>lib/sorl/thumbnail/readme.txt</filename>
    </removed>
    <removed>
      <filename>lib/tagging/validators.py</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>8b047a134511083ef6f601ff0a7d7ec99709ea12</id>
    </parent>
  </parents>
  <author>
    <name>Jeremy Dunck</name>
    <email>jdunck@gmail.com</email>
  </author>
  <url>http://github.com/simonw/djangopeople.net/commit/6d580b17e0c51b54d747eb4e49e29b01dc078ddf</url>
  <id>6d580b17e0c51b54d747eb4e49e29b01dc078ddf</id>
  <committed-date>2009-09-12T14:21:13-07:00</committed-date>
  <authored-date>2009-09-12T02:48:03-07:00</authored-date>
  <message>Ported djangopeople.net to django 1.1 (r11478).  Replaced lib/tagging and lib/sorl with recent versions.

Signed-off-by: Simon Willison &lt;simon@simonwillison.net&gt;</message>
  <tree>3fddcdd2c5e305fad95ebb895b05495c04587323</tree>
  <committer>
    <name>Simon Willison</name>
    <email>simon@simonwillison.net</email>
  </committer>
</commit>
