From: Stephen Burrows Date: Tue, 14 Jun 2011 23:24:09 +0000 (-0400) Subject: Merge branch 'master' into develop X-Git-Tag: philo-0.9.1^2~8^2~2 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/1c8fa4a2b433dc9a87814c15486929e20eaeecab?hp=51af4dbc0b6cb683480ea211ef45761d914d9945 Merge branch 'master' into develop --- diff --git a/philo/contrib/sobol/models.py b/philo/contrib/sobol/models.py index b35133e..ffe5871 100644 --- a/philo/contrib/sobol/models.py +++ b/philo/contrib/sobol/models.py @@ -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. diff --git a/philo/contrib/sobol/search.py b/philo/contrib/sobol/search.py index eb2a333..a79030a 100644 --- a/philo/contrib/sobol/search.py +++ b/philo/contrib/sobol/search.py @@ -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 index 0000000..b2e6a5e --- /dev/null +++ b/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py @@ -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'] diff --git a/philo/models/fields/__init__.py b/philo/models/fields/__init__.py index efd315f..7ab4326 100644 --- a/philo/models/fields/__init__.py +++ b/philo/models/fields/__init__.py @@ -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: diff --git a/philo/models/nodes.py b/philo/models/nodes.py index 93f772a..5b8b8ed 100644 --- a/philo/models/nodes.py +++ b/philo/models/nodes.py @@ -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 index 0000000..1673429 --- /dev/null +++ b/philo/utils/registry.py @@ -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