From: Joseph Spiros Date: Tue, 8 Feb 2011 00:47:56 +0000 (-0500) Subject: Merge branch 'master' of git://github.com/melinath/philo X-Git-Tag: philo-0.9~21 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/92aff1148db7b03f585dbe204abda452a402c20c?hp=f50cd63e40d7dfc9ffb51b679748b0003fddb019 Merge branch 'master' of git://github.com/melinath/philo * 'master' of git://github.com/melinath/philo: Added indices to all Attribute fields to improve lookup speed. Added related_lookup_fields to NodeAdmin for grappelli forward-compatibility. Tweaked LazyNode to handle trailing slashes. Corrected missing import in models/nodes.py. Corrected shipherd handling of the navigation_items related_name following commit 6ac457d4ac226a474e988dfb898682ae04a86eb0. Removed trailing_pathsep support from TreeManager.get_with_path. Set nodes to have URLs without a trailing slash, and set node_view to redirect node urls with a trailing slash to the same url, but without the slash. Resolves issue 75 completely. Added an abstract TargetURLModel to handle issues related to targeting a node, a node's subpaths, a url, or a reversable view. Addresses issue 79 and issue 76. Minor correction to LazyNode's use of subpath to avoid NameErrors. Minor cleanup to penfield.utils after get_absolute_url refactor. Refactored Node.get_absolute_url and related functions (such as MultiView.reverse) to use a new Node.construct_url function, which handles constructing any type of url involving a node or raises an appropriate error if this is not possible. Moved MultiView.reverse to View and merged View.get_subpath into it. Added APPEND_SLASH support to node_view, including support for resolving non-philo targets (resolves issue 75). Made urlpatterns for penfield and waldo MultiViews unambiguous. Altered LazyNode to always return a subpath of at least "/". Added handles_subpath methods to View and MultiView. Registered Tag as a value model. Corrected EntityAdminMetaclass handling of inherited vs. declared readonly_fields. Clarified error messages and docstrings in models/nodes.py. Removed an extraneous argument to MultiView.urlpatterns. Added navigation key to shipherd's has_navigation filter. --- diff --git a/admin/base.py b/admin/base.py index 81ff8f8..8151461 100644 --- a/admin/base.py +++ b/admin/base.py @@ -47,9 +47,13 @@ class EntityAdminMetaclass(admin.ModelAdmin.__metaclass__): opts = form._meta if issubclass(form, EntityForm) and opts.model: proxy_fields = proxy_fields_for_entity_model(opts.model).keys() + + # Store readonly fields iff they have been declared. + if 'readonly_fields' in attrs or not hasattr(new_class, '_real_readonly_fields'): + new_class._real_readonly_fields = new_class.readonly_fields + readonly_fields = new_class.readonly_fields - new_class._real_readonly_fields = readonly_fields - new_class.readonly_fields = list(readonly_fields) + proxy_fields + new_class.readonly_fields = list(set(readonly_fields) | set(proxy_fields)) # Additional HACKS to handle raw_id_fields and other attributes that the admin # uses model._meta.get_field to validate. diff --git a/admin/nodes.py b/admin/nodes.py index a576d44..fdfbc02 100644 --- a/admin/nodes.py +++ b/admin/nodes.py @@ -1,10 +1,15 @@ from django.contrib import admin -from philo.admin.base import EntityAdmin, TreeEntityAdmin +from philo.admin.base import EntityAdmin, TreeEntityAdmin, COLLAPSE_CLASSES from philo.models import Node, Redirect, File class NodeAdmin(TreeEntityAdmin): list_display = ('slug', 'view', 'accepts_subpath') + related_field_lookups = { + 'fk': [], + 'm2m': [], + 'generic': [['view_content_type', 'view_object_id']] + } def accepts_subpath(self, obj): return obj.accepts_subpath @@ -18,11 +23,19 @@ class ViewAdmin(EntityAdmin): class RedirectAdmin(ViewAdmin): fieldsets = ( (None, { - 'fields': ('target', 'status_code') + 'fields': ('target_node', 'url_or_subpath', 'status_code') }), + ('Advanced', { + 'fields': ('reversing_parameters',), + 'classes': COLLAPSE_CLASSES + }) ) - list_display = ('target', 'status_code') + list_display = ('target_url', 'status_code', 'target_node', 'url_or_subpath') list_filter = ('status_code',) + raw_id_fields = ['target_node'] + related_field_lookups = { + 'fk': ['target_node'] + } class FileAdmin(ViewAdmin): diff --git a/contrib/penfield/models.py b/contrib/penfield/models.py index 7ca879d..27e5b5d 100644 --- a/contrib/penfield/models.py +++ b/contrib/penfield/models.py @@ -111,49 +111,49 @@ class BlogView(MultiView, FeedMultiViewMixin): ) if self.feeds_enabled: urlpatterns += patterns('', - url(r'^%s/(?P[-\w]+[-+/\w]*)/%s/' % (self.tag_permalink_base, self.feed_suffix), self.feed_view(self.get_entries_by_tag, 'entries_by_tag_feed'), name='entries_by_tag_feed'), + url(r'^%s/(?P[-\w]+[-+/\w]*)/%s$' % (self.tag_permalink_base, self.feed_suffix), self.feed_view(self.get_entries_by_tag, 'entries_by_tag_feed'), name='entries_by_tag_feed'), ) urlpatterns += patterns('', - url(r'^%s/(?P[-\w]+[-+/\w]*)/' % self.tag_permalink_base, self.page_view(self.get_entries_by_tag, self.tag_page), name='entries_by_tag') + url(r'^%s/(?P[-\w]+[-+/\w]*)$' % self.tag_permalink_base, self.page_view(self.get_entries_by_tag, self.tag_page), name='entries_by_tag') ) if self.tag_archive_page: urlpatterns += patterns('', - url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view, 'tag_archive') + url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive') ) if self.entry_archive_page: if self.entry_permalink_style in 'DMY': urlpatterns += patterns('', - url(r'^(?P\d{4})/', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_year'))) + url(r'^(?P\d{4})', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_year'))) ) if self.entry_permalink_style in 'DM': urlpatterns += patterns('', - url(r'^(?P\d{4})/(?P\d{2})/?$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_month'))), + url(r'^(?P\d{4})/(?P\d{2})$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_month'))), ) if self.entry_permalink_style == 'D': urlpatterns += patterns('', - url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/?$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_day'))) + url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_day'))) ) if self.entry_permalink_style == 'D': urlpatterns += patterns('', - url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/?$', self.entry_view) + url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)$', self.entry_view) ) elif self.entry_permalink_style == 'M': urlpatterns += patterns('', - url(r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+)/?$', self.entry_view) + url(r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+)$', self.entry_view) ) elif self.entry_permalink_style == 'Y': urlpatterns += patterns('', - url(r'^(?P\d{4})/(?P[-\w]+)/?$', self.entry_view) + url(r'^(?P\d{4})/(?P[-\w]+)$', self.entry_view) ) elif self.entry_permalink_style == 'B': urlpatterns += patterns('', - url((r'^(?:%s)/(?P[-\w]+)/?$' % self.entry_permalink_base), self.entry_view) + url((r'^%s/(?P[-\w]+)$' % self.entry_permalink_base), self.entry_view) ) else: urlpatterns = patterns('', - url(r'^(?P[-\w]+)/?$', self.entry_view) + url(r'^(?P[-\w]+)$', self.entry_view) ) return urlpatterns @@ -355,44 +355,44 @@ class NewsletterView(MultiView, FeedMultiViewMixin): def urlpatterns(self): urlpatterns = patterns('', url(r'^', include(self.feed_patterns(self.get_all_articles, self.index_page, 'index'))), - url(r'^(?:%s)/(?P.+)/' % self.issue_permalink_base, include(self.feed_patterns(self.get_articles_by_issue, self.issue_page, 'issue'))) + url(r'^%s/(?P.+)' % self.issue_permalink_base, include(self.feed_patterns(self.get_articles_by_issue, self.issue_page, 'issue'))) ) if self.issue_archive_page: urlpatterns += patterns('', - url(r'^(?:%s)/$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive') + url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive') ) if self.article_archive_page: urlpatterns += patterns('', - url(r'^(?:%s)/' % self.article_permalink_base, include(self.feed_patterns(self.get_all_articles, self.article_archive_page, 'articles'))) + url(r'^%s' % self.article_permalink_base, include(self.feed_patterns(self.get_all_articles, self.article_archive_page, 'articles'))) ) if self.article_permalink_style in 'DMY': urlpatterns += patterns('', - url(r'^(?:%s)/(?P\d{4})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_year'))) + url(r'^%s/(?P\d{4})' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_year'))) ) if self.article_permalink_style in 'DM': urlpatterns += patterns('', - url(r'^(?:%s)/(?P\d{4})/(?P\d{2})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_month'))) + url(r'^%s/(?P\d{4})/(?P\d{2})' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_month'))) ) if self.article_permalink_style == 'D': urlpatterns += patterns('', - url(r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P\d{2})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_day'))) + url(r'^%s/(?P\d{4})/(?P\d{2})/(?P\d{2})' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_day'))) ) if self.article_permalink_style == 'Y': urlpatterns += patterns('', - url(r'^(?:%s)/(?P\d{4})/(?P[\w-]+)/$' % self.article_permalink_base, self.article_view) + url(r'^%s/(?P\d{4})/(?P[\w-]+)$' % self.article_permalink_base, self.article_view) ) elif self.article_permalink_style == 'M': urlpatterns += patterns('', - url(r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P[\w-]+)/$' % self.article_permalink_base, self.article_view) + url(r'^%s/(?P\d{4})/(?P\d{2})/(?P[\w-]+)$' % self.article_permalink_base, self.article_view) ) elif self.article_permalink_style == 'D': urlpatterns += patterns('', - url(r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)/$' % self.article_permalink_base, self.article_view) + url(r'^%s/(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)$' % self.article_permalink_base, self.article_view) ) else: urlpatterns += patterns('', - url(r'^(?:%s)/(?P[-\w]+)/?$' % self.article_permalink_base, self.article_view) + url(r'^%s/(?P[-\w]+)$' % self.article_permalink_base, self.article_view) ) return urlpatterns diff --git a/contrib/penfield/utils.py b/contrib/penfield/utils.py index 43c7c91..bfa08d0 100644 --- a/contrib/penfield/utils.py +++ b/contrib/penfield/utils.py @@ -1,7 +1,5 @@ from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed from django.conf.urls.defaults import url, patterns -from django.contrib.sites.models import Site -from django.core.urlresolvers import reverse from django.http import HttpResponse from philo.utils import paginate @@ -53,16 +51,14 @@ class FeedMultiViewMixin(object): else: feed_type = 'atom' - current_site = Site.objects.get_current() - #Could this be done with request.path instead somehow? feed_kwargs = { - 'link': 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse(reverse_name, urlconf=self, kwargs=kwargs).strip('/')) + 'link': request.node.construct_url(subpath=self.reverse(reverse_name, kwargs=kwargs), request=request, with_domain=True) } feed = self.get_feed(feed_type, extra_context, feed_kwargs) for obj in objects: kwargs = { - 'link': 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), self.get_subpath(obj).strip('/')) + 'link': request.node.construct_url(subpath=self.reverse(obj=obj), request=request, with_domain=True) } self.add_item(feed, obj, kwargs=kwargs) @@ -93,7 +89,7 @@ class FeedMultiViewMixin(object): if self.feeds_enabled: feed_name = '%s_feed' % base_name urlpatterns = patterns('', - url(r'^%s/$' % self.feed_suffix, self.feed_view(object_fetcher, feed_name), name=feed_name), + url(r'^%s$' % self.feed_suffix, self.feed_view(object_fetcher, feed_name), name=feed_name), ) + urlpatterns return urlpatterns diff --git a/contrib/shipherd/migrations/0001_initial.py b/contrib/shipherd/migrations/0001_initial.py new file mode 100644 index 0000000..c33d64a --- /dev/null +++ b/contrib/shipherd/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# 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): + + # Adding model 'Navigation' + db.create_table('shipherd_navigation', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('node', self.gf('django.db.models.fields.related.ForeignKey')(related_name='navigation_set', to=orm['philo.Node'])), + ('key', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('depth', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=3)), + )) + db.send_create_signal('shipherd', ['Navigation']) + + # Adding unique constraint on 'Navigation', fields ['node', 'key'] + db.create_unique('shipherd_navigation', ['node_id', 'key']) + + # Adding model 'NavigationItem' + db.create_table('shipherd_navigationitem', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['shipherd.NavigationItem'])), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)), + ('lft', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('rght', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('tree_id', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('level', self.gf('django.db.models.fields.PositiveIntegerField')(db_index=True)), + ('navigation', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='roots', null=True, to=orm['shipherd.Navigation'])), + ('text', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('target_node', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='navigation_items', null=True, to=orm['philo.Node'])), + ('url_or_subpath', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)), + ('reversing_parameters', self.gf('philo.models.fields.JSONField')(blank=True)), + ('order', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0)), + )) + db.send_create_signal('shipherd', ['NavigationItem']) + + + def backwards(self, orm): + + # Deleting model 'Navigation' + db.delete_table('shipherd_navigation') + + # Removing unique constraint on 'Navigation', fields ['node', 'key'] + db.delete_unique('shipherd_navigation', ['node_id', 'key']) + + # Deleting model 'NavigationItem' + db.delete_table('shipherd_navigationitem') + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'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', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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', [], {'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', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'shipherd.navigation': { + 'Meta': {'unique_together': "(('node', 'key'),)", 'object_name': 'Navigation'}, + 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_set'", 'to': "orm['philo.Node']"}) + }, + 'shipherd.navigationitem': { + 'Meta': {'object_name': 'NavigationItem'}, + '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'}), + 'navigation': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'roots'", 'null': 'True', 'to': "orm['shipherd.Navigation']"}), + 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['shipherd.NavigationItem']"}), + 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'navigation_items'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) + } + } + + complete_apps = ['shipherd'] diff --git a/contrib/shipherd/migrations/__init__.py b/contrib/shipherd/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/shipherd/models.py b/contrib/shipherd/models.py index dee16e9..2eacc89 100644 --- a/contrib/shipherd/models.py +++ b/contrib/shipherd/models.py @@ -4,7 +4,7 @@ from django.core.urlresolvers import NoReverseMatch from django.core.validators import RegexValidator, MinValueValidator from django.db import models from django.forms.models import model_to_dict -from philo.models import TreeEntity, JSONField, Node, TreeManager, Entity +from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel from philo.validators import RedirectValidator from UserDict import DictMixin @@ -155,7 +155,7 @@ class NavigationManager(models.Manager): # A distinct query is not strictly necessary. TODO: benchmark the efficiency # with/without distinct. - targets = list(Node.objects.filter(navigation_items__in=items).distinct()) + targets = list(Node.objects.filter(shipherd_navigationitem_related__in=items).distinct()) for cache in caches: for item in cache['items']: @@ -204,16 +204,12 @@ class NavigationItemManager(TreeManager): return NavigationCacheQuerySet(self.model, using=self._db) -class NavigationItem(TreeEntity): +class NavigationItem(TreeEntity, TargetURLModel): objects = NavigationItemManager() navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.") text = models.CharField(max_length=50) - target_node = models.ForeignKey(Node, blank=True, null=True, related_name='navigation_items', help_text="Point to this node's url.") - url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.") - reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.") - order = models.PositiveSmallIntegerField(default=0) def __init__(self, *args, **kwargs): @@ -225,41 +221,10 @@ class NavigationItem(TreeEntity): return self.get_path(field='text', pathsep=u' › ') def clean(self): - # Should this be enforced? Not enforcing it would allow creation of "headers" in the navbar. - if not self.target_node and not self.url_or_subpath: - raise ValidationError("Either a target node or a url must be defined.") - - if self.reversing_parameters and (not self.url_or_subpath or not self.target_node): - raise ValidationError("Reversing parameters require a view name and a target node.") - - try: - self.get_target_url() - except NoReverseMatch, e: - raise ValidationError(e.message) - + super(NavigationItem, self).clean() if bool(self.parent) == bool(self.navigation): raise ValidationError("Exactly one of `parent` and `navigation` must be defined.") - def get_target_url(self): - node = self.target_node - if node is not None and node.accepts_subpath and self.url_or_subpath: - if self.reversing_parameters is not None: - view_name = self.url_or_subpath - params = self.reversing_parameters - args = isinstance(params, list) and params or None - kwargs = isinstance(params, dict) and params or None - return node.view.reverse(view_name, args=args, kwargs=kwargs, node=node) - else: - subpath = self.url_or_subpath - while subpath and subpath[0] == '/': - subpath = subpath[1:] - return '%s%s' % (node.get_absolute_url(), subpath) - elif node is not None: - return node.get_absolute_url() - else: - return self.url_or_subpath - target_url = property(get_target_url) - def is_active(self, request): if self.target_url == request.path: # Handle the `default` case where the target_url and requested path diff --git a/contrib/shipherd/templatetags/shipherd.py b/contrib/shipherd/templatetags/shipherd.py index 97475fd..98e3e6b 100644 --- a/contrib/shipherd/templatetags/shipherd.py +++ b/contrib/shipherd/templatetags/shipherd.py @@ -83,7 +83,13 @@ def recursenavigation(parser, token): @register.filter -def has_navigation(node): # optional arg for a key? +def has_navigation(node, key=None): + nav = node.navigation + if key is not None: + if key in nav and bool(node.navigation[key]): + return True + elif key not in node.navigation: + return False return bool(node.navigation) diff --git a/contrib/waldo/models.py b/contrib/waldo/models.py index 2f40da7..3286aa0 100644 --- a/contrib/waldo/models.py +++ b/contrib/waldo/models.py @@ -41,32 +41,31 @@ class LoginMultiView(MultiView): @property def urlpatterns(self): urlpatterns = patterns('', - url(r'^login/$', self.login, name='login'), - url(r'^logout/$', self.logout, name='logout'), + url(r'^login$', self.login, name='login'), + url(r'^logout$', self.logout, name='logout'), - url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'), - url(r'^password/reset/(?P\w+)/(?P[^/]+)/$', self.password_reset_confirm, name='password_reset_confirm'), + url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'), + url(r'^password/reset/(?P\w+)/(?P[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'), - url(r'^register/$', csrf_protect(self.register), name='register'), - url(r'^register/(?P\w+)/(?P[^/]+)/$', self.register_confirm, name='register_confirm') + url(r'^register$', csrf_protect(self.register), name='register'), + url(r'^register/(?P\w+)/(?P[^/]+)$', self.register_confirm, name='register_confirm') ) if self.password_change_page: urlpatterns += patterns('', - url(r'^password/change/$', csrf_protect(self.login_required(self.password_change)), name='password_change'), + url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'), ) return urlpatterns def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None): - current_site = Site.objects.get_current() token = token_generator.make_token(user, *(token_args or [])) kwargs = { 'uidb36': int_to_base36(user.id), 'token': token } kwargs.update(reverse_kwargs or {}) - return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node)) + return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True) def display_login_page(self, request, message, extra_context=None): request.session.set_test_cookie() @@ -336,8 +335,8 @@ class AccountMultiView(LoginMultiView): def urlpatterns(self): urlpatterns = super(AccountMultiView, self).urlpatterns urlpatterns += patterns('', - url(r'^account/$', self.login_required(self.account_view), name='account'), - url(r'^account/email/(?P\w+)/(?P[\w.]+[+][\w.]+)/(?P[^/]+)/$', self.email_change_confirm, name='email_change_confirm') + url(r'^account$', self.login_required(self.account_view), name='account'), + url(r'^account/email/(?P\w+)/(?P[\w.]+[+][\w.]+)/(?P[^/]+)$', self.email_change_confirm, name='email_change_confirm') ) return urlpatterns diff --git a/exceptions.py b/exceptions.py index 1e4b9d9..f53083d 100644 --- a/exceptions.py +++ b/exceptions.py @@ -5,12 +5,12 @@ MIDDLEWARE_NOT_CONFIGURED = ImproperlyConfigured("""Philo requires the RequestNo class ViewDoesNotProvideSubpaths(Exception): - """ Raised by get_subpath when the View does not provide subpaths (the default). """ + """ Raised by View.reverse when the View does not provide subpaths (the default). """ silent_variable_failure = True class ViewCanNotProvideSubpath(Exception): - """ Raised by get_subpath when the View can not provide a subpath for the supplied object. """ + """ Raised by View.reverse when the View can not provide a subpath for the supplied arguments. """ silent_variable_failure = True diff --git a/middleware.py b/middleware.py index ad660ec..c0b1e9e 100644 --- a/middleware.py +++ b/middleware.py @@ -15,12 +15,24 @@ class LazyNode(object): except Site.DoesNotExist: current_site = None + path = request._cached_node_path + trailing_slash = False + if path[-1] == '/': + trailing_slash = True + try: - node, subpath = Node.objects.get_with_path(request._cached_node_path, root=getattr(current_site, 'root_node', None), absolute_result=False) + node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False) except Node.DoesNotExist: node = None if node: + if subpath is None: + subpath = "" + subpath = "/" + subpath + + if trailing_slash and subpath[-1] != "/": + subpath += "/" + node.subpath = subpath request._found_node = node diff --git a/migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py b/migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py new file mode 100644 index 0000000..dcacc79 --- /dev/null +++ b/migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py @@ -0,0 +1,151 @@ +# 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): + + # Adding field 'Redirect.target_node' + db.add_column('philo_redirect', 'target_node', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='philo_redirect_related', null=True, to=orm['philo.Node']), keep_default=False) + + # Adding field 'Redirect.url_or_subpath' + db.add_column('philo_redirect', 'url_or_subpath', self.gf('django.db.models.fields.CharField')(default='', max_length=200, blank=True), keep_default=False) + + # Adding field 'Redirect.reversing_parameters' + db.add_column('philo_redirect', 'reversing_parameters', self.gf('philo.models.fields.JSONField')(default='null', blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Redirect.target_node' + db.delete_column('philo_redirect', 'target_node_id') + + # Deleting field 'Redirect.url_or_subpath' + db.delete_column('philo_redirect', 'url_or_subpath') + + # Deleting field 'Redirect.reversing_parameters' + db.delete_column('philo_redirect', 'reversing_parameters_json') + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'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', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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', [], {'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'}), + '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'}), + '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', [], {'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'"}) + }, + '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', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + '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': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + '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': {'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/migrations/0011_move_target_url.py b/migrations/0011_move_target_url.py new file mode 100644 index 0000000..4fd4304 --- /dev/null +++ b/migrations/0011_move_target_url.py @@ -0,0 +1,141 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + for redirect in orm.Redirect.objects.all(): + redirect.url_or_subpath = redirect.target + redirect.save() + + + def backwards(self, orm): + "This will cause data loss and is not advisable. Blurg!" + for redirect in orm.Redirect.objects.all(): + redirect.target = redirect.url_or_subpath + redirect.save() + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'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', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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', [], {'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'}), + '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'}), + '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', [], {'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'"}) + }, + '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', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + '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': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + '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': {'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/migrations/0012_auto__del_field_redirect_target.py b/migrations/0012_auto__del_field_redirect_target.py new file mode 100644 index 0000000..a536ebb --- /dev/null +++ b/migrations/0012_auto__del_field_redirect_target.py @@ -0,0 +1,138 @@ +# 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): + + # Deleting field 'Redirect.target' + db.delete_column('philo_redirect', 'target') + + + def backwards(self, orm): + + # Adding field 'Redirect.target' + db.add_column('philo_redirect', 'target', self.gf('django.db.models.fields.CharField')(default='', max_length=200), keep_default=False) + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'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', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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', [], {'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'}), + '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'}), + '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', [], {'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'"}) + }, + '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', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + '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': {'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/migrations/0013_auto.py b/migrations/0013_auto.py new file mode 100644 index 0000000..c8f7799 --- /dev/null +++ b/migrations/0013_auto.py @@ -0,0 +1,150 @@ +# 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): + + # Adding index on 'Attribute', fields ['entity_object_id'] + db.create_index('philo_attribute', ['entity_object_id']) + + # Adding index on 'Attribute', fields ['value_object_id'] + db.create_index('philo_attribute', ['value_object_id']) + + # Adding index on 'Attribute', fields ['key'] + db.create_index('philo_attribute', ['key']) + + + def backwards(self, orm): + + # Removing index on 'Attribute', fields ['entity_object_id'] + db.delete_index('philo_attribute', ['entity_object_id']) + + # Removing index on 'Attribute', fields ['value_object_id'] + db.delete_index('philo_attribute', ['value_object_id']) + + # Removing index on 'Attribute', fields ['key'] + db.delete_index('philo_attribute', ['key']) + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'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'}), + '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'}), + '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', [], {'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'"}) + }, + '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', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + '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': {'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/models/base.py b/models/base.py index 290e8b8..5f8fb03 100644 --- a/models/base.py +++ b/models/base.py @@ -45,6 +45,9 @@ def register_value_model(model): value_content_type_limiter.register_class(model) +register_value_model(Tag) + + def unregister_value_model(model): value_content_type_limiter.unregister_class(model) @@ -216,15 +219,15 @@ class ManyToManyValue(AttributeValue): class Attribute(models.Model): - entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type') - entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID') + entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type', db_index=True) + entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True) entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id') - value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True) - value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True) + value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True, db_index=True) + value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True) value = generic.GenericForeignKey('value_content_type', 'value_object_id') - key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.") + key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True) def __unicode__(self): return u'"%s": %s' % (self.key, self.value) @@ -312,11 +315,6 @@ class TreeManager(models.Manager): # tree structure won't be that deep. segments = path.split(pathsep) - # Check for a trailing pathsep so we can restore it later. - trailing_pathsep = False - if segments[-1] == '': - trailing_pathsep = True - # Clean out blank segments. Handles multiple consecutive pathseps. while True: try: @@ -348,12 +346,6 @@ class TreeManager(models.Manager): return kwargs - def build_path(segments): - path = pathsep.join(segments) - if trailing_pathsep and segments and segments[-1] != '': - path += pathsep - return path - def find_obj(segments, depth, deepest_found=None): if deepest_found is None: deepest_level = 0 @@ -374,7 +366,7 @@ class TreeManager(models.Manager): if deepest_level == depth: # This should happen if nothing is found with any part of the given path. if root is not None and deepest_found is None: - return root, build_path(segments) + return root, pathsep.join(segments) raise return find_obj(segments, depth, deepest_found) @@ -387,7 +379,7 @@ class TreeManager(models.Manager): # Could there be a deeper one? if obj.is_leaf_node(): - return obj, build_path(segments[deepest_level:]) or None + return obj, pathsep.join(segments[deepest_level:]) or None depth += (len(segments) - depth)/2 or len(segments) - depth @@ -395,13 +387,13 @@ class TreeManager(models.Manager): depth = deepest_level + obj.get_descendant_count() if deepest_level == depth: - return obj, build_path(segments[deepest_level:]) or None + return obj, pathsep.join(segments[deepest_level:]) or None try: return find_obj(segments, depth, obj) except self.model.DoesNotExist: # Then this was the deepest. - return obj, build_path(segments[deepest_level:]) + return obj, pathsep.join(segments[deepest_level:]) if absolute_result: return self.get(**make_query_kwargs(segments, root)) diff --git a/models/nodes.py b/models/nodes.py index 2bfb4fd..09376b8 100644 --- a/models/nodes.py +++ b/models/nodes.py @@ -1,15 +1,16 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic -from django.contrib.sites.models import Site -from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect -from django.core.exceptions import ViewDoesNotExist +from django.contrib.sites.models import Site, RequestSite +from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404 +from django.core.exceptions import ValidationError from django.core.servers.basehttp import FileWrapper from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch from django.template import add_to_builtins as register_templatetags from inspect import getargspec from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model +from philo.models.fields import JSONField from philo.utils import ContentTypeSubclassLimiter from philo.validators import RedirectValidator from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist @@ -30,23 +31,53 @@ class Node(TreeEntity): return self.view.accepts_subpath return False + def handles_subpath(self, subpath): + return self.view.handles_subpath(subpath) + def render_to_response(self, request, extra_context=None): return self.view.render_to_response(request, extra_context) - def get_absolute_url(self): + def get_absolute_url(self, request=None, with_domain=False, secure=False): + return self.construct_url(request=request, with_domain=with_domain, secure=secure) + + def construct_url(self, subpath="/", request=None, with_domain=False, secure=False): + """ + This method will construct a URL based on the Node's location. + If a request is passed in, that will be used as a backup in case + the Site lookup fails. The Site lookup takes precedence because + it's what's used to find the root node. This will raise: + - NoReverseMatch if philo-root is not reverseable + - Site.DoesNotExist if a domain is requested but not buildable. + - AncestorDoesNotExist if the root node of the site isn't an + ancestor of this instance. + """ + # Try reversing philo-root first, since we can't do anything if that fails. + root_url = reverse('philo-root') + try: - root = Site.objects.get_current().root_node + current_site = Site.objects.get_current() except Site.DoesNotExist: - root = None + if request is not None: + current_site = RequestSite(request) + elif with_domain: + # If they want a domain and we can't figure one out, + # best to reraise the error to let them know. + raise + else: + current_site = None - try: - path = self.get_path(root=root) - if path: - path += '/' - root_url = reverse('philo-root') - return '%s%s' % (root_url, path) - except AncestorDoesNotExist, ViewDoesNotExist: - return None + root = getattr(current_site, 'root_node', None) + path = self.get_path(root=root) + + if current_site and with_domain: + domain = "http%s://%s" % (secure and "s" or "", current_site.domain) + else: + domain = "" + + if not path or subpath == "/": + subpath = subpath[1:] + + return '%s%s%s%s' % (domain, root_url, path, subpath) class Meta: app_label = 'philo' @@ -61,15 +92,34 @@ class View(Entity): accepts_subpath = False - def get_subpath(self, obj): + def handles_subpath(self, subpath): + if not self.accepts_subpath and subpath != "/": + return False + return True + + def reverse(self, view_name=None, args=None, kwargs=None, node=None, obj=None): + """Shortcut method to handle the common pattern of getting the + absolute url for a view's subpaths.""" if not self.accepts_subpath: raise ViewDoesNotProvideSubpaths - view_name, args, kwargs = self.get_reverse_params(obj) + if obj is not None: + # Perhaps just override instead of combining? + obj_view_name, obj_args, obj_kwargs = self.get_reverse_params(obj) + if view_name is None: + view_name = obj_view_name + args = list(obj_args) + list(args or []) + obj_kwargs.update(kwargs or {}) + kwargs = obj_kwargs + try: - return reverse(view_name, args=args, kwargs=kwargs, urlconf=self) + subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {}) except NoReverseMatch: raise ViewCanNotProvideSubpath + + if node is not None: + return node.construct_url(subpath) + return subpath def get_reverse_params(self, obj): """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf.""" @@ -89,7 +139,7 @@ class View(Entity): return response def actually_render_to_response(self, request, extra_context=None): - raise NotImplementedError('View subclasses must implement render_to_response.') + raise NotImplementedError('View subclasses must implement actually_render_to_response.') class Meta: abstract = True @@ -102,15 +152,21 @@ class MultiView(View): accepts_subpath = True @property - def urlpatterns(self, obj): + def urlpatterns(self): raise NotImplementedError("MultiView subclasses must implement urlpatterns.") + def handles_subpath(self, subpath): + if not super(MultiView, self).handles_subpath(subpath): + return False + try: + resolve(subpath, urlconf=self) + except Http404: + return False + return True + def actually_render_to_response(self, request, extra_context=None): clear_url_caches() subpath = request.node.subpath - if not subpath: - subpath = "" - subpath = "/" + subpath view, args, kwargs = resolve(subpath, urlconf=self) view_args = getargspec(view) if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None): @@ -119,25 +175,20 @@ class MultiView(View): kwargs['extra_context'] = extra_context return view(request, *args, **kwargs) - def reverse(self, view_name, args=None, kwargs=None, node=None): - """Shortcut method to handle the common pattern of getting the absolute url for a multiview's - subpaths.""" - subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {}) - if node is not None: - return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/')) - return subpath - def get_context(self): """Hook for providing instance-specific context - such as the value of a Field - to all views.""" return {} - def basic_view(self, view_name): + def basic_view(self, field_name): """ - Wraps a field name and returns a simple view function that will render that view - with a basic context. This assumes that the field name is a ForeignKey to a - model with a render_to_response method. + Given the name of a field on ``self``, accesses the value of + that field and treats it as a ``View`` instance. Creates a + basic context based on self.get_context() and any extra_context + that was passed in, then calls the ``View`` instance's + render_to_response() method. This method is meant to be called + to return a view function appropriate for urlpatterns. """ - field = self._meta.get_field(view_name) + field = self._meta.get_field(field_name) view = getattr(self, field.name, None) def inner(request, extra_context=None, **kwargs): @@ -153,16 +204,65 @@ class MultiView(View): abstract = True -class Redirect(View): +class TargetURLModel(models.Model): + target_node = models.ForeignKey(Node, blank=True, null=True, related_name="%(app_label)s_%(class)s_related") + url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.") + reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.") + + def clean(self): + # Should this be enforced? Not enforcing it would allow creation of "headers" in the navbar. + if not self.target_node and not self.url_or_subpath: + raise ValidationError("Either a target node or a url must be defined.") + + if self.reversing_parameters and not (self.url_or_subpath or self.target_node): + raise ValidationError("Reversing parameters require either a view name or a target node.") + + try: + self.get_target_url() + except NoReverseMatch, e: + raise ValidationError(e.message) + + super(TargetURLModel, self).clean() + + def get_reverse_params(self): + params = self.reversing_parameters + args = isinstance(params, list) and params or None + kwargs = isinstance(params, dict) and params or None + return self.url_or_subpath, args, kwargs + + def get_target_url(self): + node = self.target_node + if node is not None and node.accepts_subpath and self.url_or_subpath: + if self.reversing_parameters is not None: + view_name, args, kwargs = self.get_reversing_params() + subpath = node.view.reverse(view_name, args=args, kwargs=kwargs) + else: + subpath = self.url_or_subpath + if subpath[0] != '/': + subpath = '/' + subpath + return node.construct_url(subpath) + elif node is not None: + return node.get_absolute_url() + else: + if self.reversing_parameters is not None: + view_name, args, kwargs = self.get_reversing_params() + return reverse(view_name, args=args, kwargs=kwargs) + return self.url_or_subpath + target_url = property(get_target_url) + + class Meta: + abstract = True + + +class Redirect(View, TargetURLModel): STATUS_CODES = ( (302, 'Temporary'), (301, 'Permanent'), ) - target = models.CharField(max_length=200, validators=[RedirectValidator()]) status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type') def actually_render_to_response(self, request, extra_context=None): - response = HttpResponseRedirect(self.target) + response = HttpResponseRedirect(self.target_url) response.status_code = self.status_code return response @@ -170,7 +270,6 @@ class Redirect(View): app_label = 'philo' -# Why does this exist? class File(View): """ For storing arbitrary files """ diff --git a/templatetags/nodes.py b/templatetags/nodes.py index 73492d4..5ae507d 100644 --- a/templatetags/nodes.py +++ b/templatetags/nodes.py @@ -55,10 +55,7 @@ class NodeURLNode(template.Node): raise return settings.TEMPLATE_STRING_IF_INVALID else: - if subpath[0] == '/': - subpath = subpath[1:] - - url = node.get_absolute_url() + subpath + url = node.construct_url(subpath) if self.as_var: context[self.as_var] = url diff --git a/views.py b/views.py index 255e54e..f5a2c7f 100644 --- a/views.py +++ b/views.py @@ -1,5 +1,6 @@ from django.conf import settings -from django.http import Http404 +from django.core.urlresolvers import resolve +from django.http import Http404, HttpResponseRedirect from django.views.decorators.vary import vary_on_headers from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED @@ -10,11 +11,50 @@ def node_view(request, path=None, **kwargs): raise MIDDLEWARE_NOT_CONFIGURED if not request.node: + if settings.APPEND_SLASH and request.path != "/": + path = request.path + + if path[-1] == "/": + path = path[:-1] + else: + path += "/" + + view, args, kwargs = resolve(path) + if view != node_view: + return HttpResponseRedirect(path) raise Http404 node = request.node subpath = request.node.subpath - if subpath and not node.accepts_subpath: - raise Http404 + # Explicitly disallow trailing slashes if we are otherwise at a node's url. + if request.path and request.path != "/" and request.path[-1] == "/" and subpath == "/": + return HttpResponseRedirect(node.get_absolute_url()) + + if not node.handles_subpath(subpath): + # If the subpath isn't handled, check settings.APPEND_SLASH. If + # it's True, try to correct the subpath. + if not settings.APPEND_SLASH: + raise Http404 + + if subpath[-1] == "/": + subpath = subpath[:-1] + else: + subpath += "/" + + redirect_url = node.construct_url(subpath) + + if node.handles_subpath(subpath): + return HttpResponseRedirect(redirect_url) + + # Perhaps there is a non-philo view at this address. Can we + # resolve *something* there besides node_view? If not, + # raise a 404. + view, args, kwargs = resolve(redirect_url) + + if view == node_view: + raise Http404 + else: + return HttpResponseRedirect(redirect_url) + return node.render_to_response(request, kwargs) \ No newline at end of file