Added an abstract TargetURLModel to handle issues related to targeting a node, a...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 3 Feb 2011 16:41:46 +0000 (11:41 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 3 Feb 2011 16:54:22 +0000 (11:54 -0500)
admin/nodes.py
contrib/shipherd/migrations/0001_initial.py [new file with mode: 0644]
contrib/shipherd/migrations/__init__.py [new file with mode: 0644]
contrib/shipherd/models.py
migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py [new file with mode: 0644]
migrations/0011_move_target_url.py [new file with mode: 0644]
migrations/0012_auto__del_field_redirect_target.py [new file with mode: 0644]
models/nodes.py

index a576d44..dc5ca60 100644 (file)
@@ -1,5 +1,5 @@
 from django.contrib import admin
 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
 
 
 from philo.models import Node, Redirect, File
 
 
@@ -18,11 +18,19 @@ class ViewAdmin(EntityAdmin):
 class RedirectAdmin(ViewAdmin):
        fieldsets = (
                (None, {
 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',)
        list_filter = ('status_code',)
+       raw_id_fields = ['target_node']
+       related_field_lookups = {
+               'fk': ['target_node']
+       }
 
 
 class FileAdmin(ViewAdmin):
 
 
 class FileAdmin(ViewAdmin):
diff --git a/contrib/shipherd/migrations/0001_initial.py b/contrib/shipherd/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..c33d64a
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
index 010aa9f..bddaef3 100644 (file)
@@ -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 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
 
 from philo.validators import RedirectValidator
 from UserDict import DictMixin
 
@@ -155,6 +155,7 @@ class NavigationManager(models.Manager):
                
                # A distinct query is not strictly necessary. TODO: benchmark the efficiency
                # with/without distinct.
                
                # A distinct query is not strictly necessary. TODO: benchmark the efficiency
                # with/without distinct.
+               #change to navigation_navigation_items
                targets = list(Node.objects.filter(navigation_items__in=items).distinct())
                
                for cache in caches:
                targets = list(Node.objects.filter(navigation_items__in=items).distinct())
                
                for cache in caches:
@@ -204,16 +205,12 @@ class NavigationItemManager(TreeManager):
                return NavigationCacheQuerySet(self.model, using=self._db)
 
 
                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)
        
        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):
        order = models.PositiveSmallIntegerField(default=0)
        
        def __init__(self, *args, **kwargs):
@@ -225,41 +222,10 @@ class NavigationItem(TreeEntity):
                return self.get_path(field='text', pathsep=u' › ')
        
        def clean(self):
                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.")
        
                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
-                               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:
-                       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
        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/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 (file)
index 0000000..dcacc79
--- /dev/null
@@ -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 (file)
index 0000000..4fd4304
--- /dev/null
@@ -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 (file)
index 0000000..a536ebb
--- /dev/null
@@ -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']
index ab8adf7..a89d653 100644 (file)
@@ -9,6 +9,7 @@ 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 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
 from philo.utils import ContentTypeSubclassLimiter
 from philo.validators import RedirectValidator
 from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
@@ -38,7 +39,7 @@ class Node(TreeEntity):
        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 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=None, request=None, with_domain=False, secure=False):
+       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
                """
                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
@@ -75,7 +76,7 @@ class Node(TreeEntity):
                if not path:
                        subpath = subpath[1:]
                
                if not path:
                        subpath = subpath[1:]
                
-               return '%s%s%s%s' % (domain, root_url, path, subpath or "")
+               return '%s%s%s%s' % (domain, root_url, path, subpath)
        
        class Meta:
                app_label = 'philo'
        
        class Meta:
                app_label = 'philo'
@@ -202,16 +203,65 @@ class MultiView(View):
                abstract = True
 
 
                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'),
        )
        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):
        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
        
                response.status_code = self.status_code
                return response