From 6ac457d4ac226a474e988dfb898682ae04a86eb0 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Thu, 3 Feb 2011 11:41:46 -0500 Subject: [PATCH] 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. --- admin/nodes.py | 14 +- contrib/shipherd/migrations/0001_initial.py | 108 +++++++++++++ contrib/shipherd/migrations/__init__.py | 0 contrib/shipherd/models.py | 42 +---- ..._node__add_field_redirect_url_or_subpat.py | 151 ++++++++++++++++++ migrations/0011_move_target_url.py | 141 ++++++++++++++++ .../0012_auto__del_field_redirect_target.py | 138 ++++++++++++++++ models/nodes.py | 60 ++++++- 8 files changed, 608 insertions(+), 46 deletions(-) create mode 100644 contrib/shipherd/migrations/0001_initial.py create mode 100644 contrib/shipherd/migrations/__init__.py create mode 100644 migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py create mode 100644 migrations/0011_move_target_url.py create mode 100644 migrations/0012_auto__del_field_redirect_target.py diff --git a/admin/nodes.py b/admin/nodes.py index a576d44..dc5ca60 100644 --- a/admin/nodes.py +++ b/admin/nodes.py @@ -1,5 +1,5 @@ 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 @@ -18,11 +18,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/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 010aa9f..bddaef3 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,6 +155,7 @@ class NavigationManager(models.Manager): # 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: @@ -204,16 +205,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 +222,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 - 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 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/models/nodes.py b/models/nodes.py index ab8adf7..a89d653 100644 --- a/models/nodes.py +++ b/models/nodes.py @@ -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 philo.models.fields import JSONField 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 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 @@ -75,7 +76,7 @@ class Node(TreeEntity): 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' @@ -202,16 +203,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 -- 2.20.1