Merge branch 'master' into develop
authorStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 14 Jun 2011 23:24:09 +0000 (19:24 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 14 Jun 2011 23:24:09 +0000 (19:24 -0400)
philo/contrib/sobol/models.py
philo/contrib/sobol/search.py
philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py [new file with mode: 0644]
philo/models/fields/__init__.py
philo/models/nodes.py
philo/utils/registry.py [new file with mode: 0644]

index b35133e..ffe5871 100644 (file)
@@ -153,18 +153,6 @@ class Click(models.Model):
                get_latest_by = 'datetime'
 
 
-class RegistryChoiceField(SlugMultipleChoiceField):
-       def _get_choices(self):
-               if isinstance(self._choices, RegistryIterator):
-                       return self._choices.copy()
-               elif hasattr(self._choices, 'next'):
-                       choices, self._choices = itertools.tee(self._choices)
-                       return choices
-               else:
-                       return self._choices
-       choices = property(_get_choices)
-
-
 try:
        from south.modelsinspector import add_introspection_rules
 except ImportError:
@@ -177,8 +165,8 @@ class SearchView(MultiView):
        """Handles a view for the results of a search, anonymously tracks the selections made by end users, and provides an AJAX API for asynchronous search result loading. This can be particularly useful if some searches are slow."""
        #: :class:`ForeignKey` to a :class:`.Page` which will be used to render the search results.
        results_page = models.ForeignKey(Page, related_name='search_results_related')
-       #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of the :class:`.SearchRegistry`
-       searches = RegistryChoiceField(choices=registry.iterchoices())
+       #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of :obj:`.sobol.search.registry`
+       searches = SlugMultipleChoiceField(choices=registry.iterchoices())
        #: A :class:`BooleanField` which controls whether or not the AJAX API is enabled.
        #:
        #: .. note:: If the AJAX API is enabled, a ``ajax_api_url`` attribute will be added to each search instance containing the url and get parameters for an AJAX request to retrieve results for that search.
index eb2a333..a79030a 100644 (file)
@@ -12,7 +12,8 @@ from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
 from django.template import loader, Context, Template, TemplateDoesNotExist
 
-from philo.contrib.sobol.utils import make_tracking_querydict, RegistryIterator
+from philo.contrib.sobol.utils import make_tracking_querydict
+from philo.utils.registry import Registry
 
 
 if getattr(settings, 'SOBOL_USE_EVENTLET', False):
@@ -25,7 +26,7 @@ else:
 
 
 __all__ = (
-       'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'SearchRegistry', 'registry', 'get_search_instance'
+       'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'registry', 'get_search_instance'
 )
 
 
@@ -33,74 +34,8 @@ SEARCH_CACHE_SEED = 'philo_sobol_search_results'
 USE_CACHE = getattr(settings, 'SOBOL_USE_CACHE', True)
 
 
-class RegistrationError(Exception):
-       """Raised if there is a problem registering a search with a :class:`SearchRegistry`"""
-       pass
-
-
-class SearchRegistry(object):
-       """Holds a registry of search types by slug."""
-       
-       def __init__(self):
-               self._registry = {}
-       
-       def register(self, search, slug=None):
-               """
-               Register a search with the registry.
-               
-               :param search: The search class to register - generally a subclass of :class:`BaseSearch`
-               :param slug: The slug which will be used to register the search class. If ``slug`` is ``None``, the search's default slug will be used.
-               :raises: :class:`RegistrationError` if a different search is already registered with ``slug``.
-               
-               """
-               slug = slug or search.slug
-               if slug in self._registry:
-                       registered = self._registry[slug]
-                       if registered.__module__ != search.__module__:
-                               raise RegistrationError("A different search is already registered as `%s`" % slug)
-               else:
-                       self._registry[slug] = search
-       
-       def unregister(self, search, slug=None):
-               """
-               Unregister a search from the registry.
-               
-               :param search: The search class to unregister - generally a subclass of :class:`BaseSearch`
-               :param slug: If provided, the search will only be removed if it was registered with ``slug``. If not provided, the search class will be unregistered no matter what slug it was registered with.
-               :raises: :class:`RegistrationError` if a slug is provided but the search registered with that slug is not ``search``.
-               
-               """
-               if slug is not None:
-                       if slug in self._registry and self._registry[slug] == search:
-                               del self._registry[slug]
-                       raise RegistrationError("`%s` is not registered as `%s`" % (search, slug))
-               else:
-                       for slug, search in self._registry.items():
-                               if search == search:
-                                       del self._registry[slug]
-       
-       def items(self):
-               """Returns a list of (slug, search) items in the registry."""
-               return self._registry.items()
-       
-       def iteritems(self):
-               """Returns an iterator over the (slug, search) pairs in the registry."""
-               return RegistryIterator(self._registry, 'iteritems')
-       
-       def iterchoices(self):
-               """Returns an iterator over (slug, search.verbose_name) pairs for the registry."""
-               return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1].verbose_name))
-       
-       def __getitem__(self, key):
-               """Returns the search registered with ``key``."""
-               return self._registry[key]
-       
-       def __iter__(self):
-               """Returns an iterator over the keys in the registry."""
-               return self._registry.__iter__()
-
-
-registry = SearchRegistry()
+#: A registry for :class:`BaseSearch` subclasses that should be available in the admin.
+registry = Registry()
 
 
 def _make_cache_key(search, search_arg):
@@ -119,7 +54,6 @@ def get_search_instance(slug, search_arg):
        instance = search(search_arg)
        instance.slug = slug
        return instance
-       
 
 
 class Result(object):
diff --git a/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py b/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py
new file mode 100644 (file)
index 0000000..b2e6a5e
--- /dev/null
@@ -0,0 +1,144 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Changing field 'Node.view_object_id'
+        db.alter_column('philo_node', 'view_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True))
+
+        # Changing field 'Node.view_content_type'
+        db.alter_column('philo_node', 'view_content_type_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['contenttypes.ContentType']))
+
+
+    def backwards(self, orm):
+        
+        # User chose to not deal with backwards NULL issues for 'Node.view_object_id'
+        raise RuntimeError("Cannot reverse this migration. 'Node.view_object_id' and its values cannot be restored.")
+
+        # User chose to not deal with backwards NULL issues for 'Node.view_content_type'
+        raise RuntimeError("Cannot reverse this migration. 'Node.view_content_type' and its values cannot be restored.")
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.foreignkeyvalue': {
+            'Meta': {'object_name': 'ForeignKeyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'philo.jsonvalue': {
+            'Meta': {'object_name': 'JSONValue'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+        },
+        'philo.manytomanyvalue': {
+            'Meta': {'object_name': 'ManyToManyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
index efd315f..7ab4326 100644 (file)
@@ -7,6 +7,7 @@ from django.utils.text import capfirst
 from django.utils.translation import ugettext_lazy as _
 
 from philo.forms.fields import JSONFormField
+from philo.utils.registry import RegistryIterator
 from philo.validators import TemplateValidator, json_validator
 #from philo.models.fields.entities import *
 
@@ -71,7 +72,7 @@ class JSONField(models.TextField):
 
 
 class SlugMultipleChoiceField(models.Field):
-       """Stores a selection of multiple items with unique slugs in the form of a comma-separated list."""
+       """Stores a selection of multiple items with unique slugs in the form of a comma-separated list. Also knows how to correctly handle :class:`RegistryIterator`\ s passed in as choices."""
        __metaclass__ = models.SubfieldBase
        description = _("Comma-separated slug field")
        
@@ -127,6 +128,16 @@ class SlugMultipleChoiceField(models.Field):
                if invalid_values:
                        # should really make a custom message.
                        raise ValidationError(self.error_messages['invalid_choice'] % invalid_values)
+       
+       def _get_choices(self):
+               if isinstance(self._choices, RegistryIterator):
+                       return self._choices.copy()
+               elif hasattr(self._choices, 'next'):
+                       choices, self._choices = itertools.tee(self._choices)
+                       return choices
+               else:
+                       return self._choices
+       choices = property(_get_choices)
 
 
 try:
index 93f772a..5b8b8ed 100644 (file)
@@ -31,8 +31,8 @@ class Node(SlugTreeEntity):
        :class:`Node`\ s are the basic building blocks of a website using Philo. They define the URL hierarchy and connect each URL to a :class:`View` subclass instance which is used to generate an HttpResponse.
        
        """
-       view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
-       view_object_id = models.PositiveIntegerField()
+       view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter, blank=True, null=True)
+       view_object_id = models.PositiveIntegerField(blank=True, null=True)
        #: :class:`GenericForeignKey` to a non-abstract subclass of :class:`View`
        view = generic.GenericForeignKey('view_content_type', 'view_object_id')
        
@@ -44,11 +44,15 @@ class Node(SlugTreeEntity):
                return False
        
        def handles_subpath(self, subpath):
-               return self.view.handles_subpath(subpath)
+               if self.view:
+                       return self.view.handles_subpath(subpath)
+               return False
        
        def render_to_response(self, request, extra_context=None):
                """This is a shortcut method for :meth:`View.render_to_response`"""
-               return self.view.render_to_response(request, extra_context)
+               if self.view:
+                       return self.view.render_to_response(request, extra_context)
+               raise Http404
        
        def get_absolute_url(self, request=None, with_domain=False, secure=False):
                """
diff --git a/philo/utils/registry.py b/philo/utils/registry.py
new file mode 100644 (file)
index 0000000..1673429
--- /dev/null
@@ -0,0 +1,141 @@
+from django.core.validators import slug_re
+from django.template.defaultfilters import slugify
+from django.utils.encoding import smart_str
+
+
+class RegistryIterator(object):
+       """
+       Wraps the iterator returned by calling ``getattr(registry, iterattr)`` to provide late instantiation of the wrapped iterator and to allow copying of the iterator for even later instantiation.
+       
+       :param registry: The object which provides the iterator at ``iterattr``.
+       :param iterattr: The name of the method on ``registry`` that provides the iterator.
+       :param transform: A function which will be called on each result from the wrapped iterator before it is returned.
+       
+       """
+       def __init__(self, registry, iterattr='__iter__', transform=lambda x:x):
+               if not hasattr(registry, iterattr):
+                       raise AttributeError("Registry has no attribute %s" % iterattr)
+               self.registry = registry
+               self.iterattr = iterattr
+               self.transform = transform
+       
+       def __iter__(self):
+               return self
+       
+       def next(self):
+               if not hasattr(self, '_iter'):
+                       self._iter = getattr(self.registry, self.iterattr)()
+               
+               return self.transform(self._iter.next())
+       
+       def copy(self):
+               """Returns a fresh copy of this iterator."""
+               return self.__class__(self.registry, self.iterattr, self.transform)
+
+
+class RegistrationError(Exception):
+       """Raised if there is a problem registering a object with a :class:`Registry`"""
+       pass
+
+
+class Registry(object):
+       """Holds a registry of arbitrary objects by slug."""
+       
+       def __init__(self):
+               self._registry = {}
+       
+       def register(self, obj, slug=None, verbose_name=None):
+               """
+               Register an object with the registry.
+               
+               :param obj: The object to register.
+               :param slug: The slug which will be used to register the object. If ``slug`` is ``None``, it will be generated from ``verbose_name`` or looked for at ``obj.slug``.
+               :param verbose_name: The verbose name for the object. If ``verbose_name`` is ``None``, it will be looked for at ``obj.verbose_name``.
+               :raises: :class:`RegistrationError` if a different object is already registered with ``slug``, or if ``slug`` is not a valid slug.
+               
+               """
+               verbose_name = verbose_name if verbose_name is not None else obj.verbose_name
+               
+               if slug is None:
+                       slug = getattr(obj, 'slug', slugify(verbose_name))
+               slug = smart_str(slug)
+               
+               if not slug_re.search(slug):
+                       raise RegistrationError(u"%s is not a valid slug." % slug)
+               
+               
+               if slug in self._registry:
+                       reg = self._registry[slug]
+                       if reg['obj'] != obj:
+                               raise RegistrationError(u"A different object is already registered as `%s`" % slug)
+               else:
+                       self._registry[slug] = {
+                               'obj': obj,
+                               'verbose_name': verbose_name
+                       }
+       
+       def unregister(self, obj, slug=None):
+               """
+               Unregister an object from the registry.
+               
+               :param obj: The object to unregister.
+               :param slug: If provided, the object will only be removed if it was registered with ``slug``. If not provided, the object will be unregistered no matter what slug it was registered with.
+               :raises: :class:`RegistrationError` if ``slug`` is provided and an object other than ``obj`` is registered as ``slug``.
+               
+               """
+               if slug is not None:
+                       if slug in self._registry:
+                               if self._registry[slug]['obj'] == obj:
+                                       del self._registry[slug]
+                               else:
+                                       raise RegistrationError(u"`%s` is not registered as `%s`" % (obj, slug))
+               else:
+                       for slug, reg in self.items():
+                               if obj == reg:
+                                       del self._registry[slug]
+       
+       def items(self):
+               """Returns a list of (slug, obj) items in the registry."""
+               return [(slug, self[slug]) for slug in self._registry]
+       
+       def values(self):
+               """Returns a list of objects in the registry."""
+               return [self[slug] for slug in self._registry]
+       
+       def iteritems(self):
+               """Returns a :class:`RegistryIterator` over the (slug, obj) pairs in the registry."""
+               return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['obj']))
+       
+       def itervalues(self):
+               """Returns a :class:`RegistryIterator` over the objects in the registry."""
+               return RegistryIterator(self._registry, 'itervalues', lambda x: x['obj'])
+       
+       def iterchoices(self):
+               """Returns a :class:`RegistryIterator` over (slug, verbose_name) pairs for the registry."""
+               return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['verbose_name']))
+       choices = property(iterchoices)
+       
+       def get(self, key, default=None):
+               """Returns the object registered with ``key`` or ``default`` if no object was registered."""
+               try:
+                       return self[key]
+               except KeyError:
+                       return default
+       
+       def get_slug(self, obj, default=None):
+               """Returns the slug used to register ``obj`` or ``default`` if ``obj`` was not registered."""
+               for slug, reg in self.iteritems():
+                       if obj == reg:
+                               return slug
+               return default
+       
+       def __getitem__(self, key):
+               """Returns the obj registered with ``key``."""
+               return self._registry[key]['obj']
+       
+       def __iter__(self):
+               """Returns an iterator over the keys in the registry."""
+               return self._registry.__iter__()
+       
+       def __contains__(self, item):
+               return self._registry.__contains__(item)
\ No newline at end of file