From be40499f02a3457bb17a8dc02a78224823eb1406 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Tue, 21 Dec 2010 17:36:36 -0500 Subject: [PATCH] Moved all navigation-related code to contrib/navigation. Refactored navigation to be more independant of the Node tree structure and allow for absolute urls, links to nodes, links to node subpaths, and links to reversed node subpaths. --- admin/nodes.py | 28 +--- contrib/navigation/__init__.py | 0 contrib/navigation/admin.py | 65 ++++++++ contrib/navigation/migrations/0001_initial.py | 85 ++++++++++ .../0002_auto__chg_field_navigation_text.py | 69 ++++++++ contrib/navigation/migrations/__init__.py | 0 contrib/navigation/models.py | 139 ++++++++++++++++ forms.py | 57 +------ .../0010_auto__add_nodenavigationoverride.py | 157 ------------------ models/nodes.py | 100 ----------- models/pages.py | 7 - templatetags/nodes.py | 82 +-------- 12 files changed, 364 insertions(+), 425 deletions(-) create mode 100644 contrib/navigation/__init__.py create mode 100644 contrib/navigation/admin.py create mode 100644 contrib/navigation/migrations/0001_initial.py create mode 100644 contrib/navigation/migrations/0002_auto__chg_field_navigation_text.py create mode 100644 contrib/navigation/migrations/__init__.py create mode 100644 contrib/navigation/models.py delete mode 100644 migrations/0010_auto__add_nodenavigationoverride.py diff --git a/admin/nodes.py b/admin/nodes.py index 0fac7ad..45a3172 100644 --- a/admin/nodes.py +++ b/admin/nodes.py @@ -1,32 +1,10 @@ from django.contrib import admin -from philo.admin.base import EntityAdmin, TreeEntityAdmin, COLLAPSE_CLASSES -from philo.models import Node, Redirect, File, NodeNavigationOverride -from philo.forms import NodeWithOverrideForm, NodeOverrideInlineFormSet - - -class ChildNavigationOverrideInline(admin.StackedInline): - fk_name = 'parent' - model = NodeNavigationOverride - formset = NodeOverrideInlineFormSet - sortable_field_name = 'order' - verbose_name = 'child' - verbose_name_plural = 'children' - extra = 0 - max_num = 0 +from philo.admin.base import EntityAdmin, TreeEntityAdmin +from philo.models import Node, Redirect, File class NodeAdmin(TreeEntityAdmin): - form = NodeWithOverrideForm - fieldsets = ( - (None, { - 'fields': ('parent', 'slug', 'view_content_type', 'view_object_id'), - }), - ('Navigation Overrides', { - 'fields': ('title', 'url', 'child_navigation'), - 'classes': COLLAPSE_CLASSES - }) - ) - inlines = [ChildNavigationOverrideInline] + TreeEntityAdmin.inlines + pass class ViewAdmin(EntityAdmin): diff --git a/contrib/navigation/__init__.py b/contrib/navigation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/navigation/admin.py b/contrib/navigation/admin.py new file mode 100644 index 0000000..7a43d45 --- /dev/null +++ b/contrib/navigation/admin.py @@ -0,0 +1,65 @@ +from django.contrib import admin +from philo.admin import TreeEntityAdmin, COLLAPSE_CLASSES, NodeAdmin +from philo.contrib.navigation.models import Navigation + + +NAVIGATION_RAW_ID_FIELDS = ('hosting_node', 'parent', 'target_node') + + +class NavigationInline(admin.StackedInline): + fieldsets = ( + (None, { + 'fields': ('text',) + }), + ('Target', { + 'fields': ('target_node', 'url_or_subpath',) + }), + ('Advanced', { + 'fields': ('reversing_parameters', 'order', 'depth'), + 'classes': COLLAPSE_CLASSES + }), + ('Expert', { + 'fields': ('hosting_node', 'parent'), + 'classes': COLLAPSE_CLASSES + }) + ) + raw_id_fields = NAVIGATION_RAW_ID_FIELDS + model = Navigation + extra = 1 + sortable_field_name = 'order' + + +class NavigationNavigationInline(NavigationInline): + verbose_name = "child" + verbose_name_plural = "children" + + +class NodeNavigationInline(NavigationInline): + verbose_name_plural = 'navigation' + + +class NavigationAdmin(TreeEntityAdmin): + fieldsets = ( + (None, { + 'fields': ('text', 'hosting_node',) + }), + ('Target', { + 'fields': ('target_node', 'url_or_subpath',) + }), + ('Advanced', { + 'fields': ('reversing_parameters', 'depth'), + 'classes': COLLAPSE_CLASSES + }), + ('Expert', { + 'fields': ('parent', 'order'), + 'classes': COLLAPSE_CLASSES + }) + ) + raw_id_fields = NAVIGATION_RAW_ID_FIELDS + inlines = [NavigationNavigationInline] + TreeEntityAdmin.inlines + + +NodeAdmin.inlines = [NodeNavigationInline] + NodeAdmin.inlines + + +admin.site.register(Navigation, NavigationAdmin) \ No newline at end of file diff --git a/contrib/navigation/migrations/0001_initial.py b/contrib/navigation/migrations/0001_initial.py new file mode 100644 index 0000000..d60c123 --- /dev/null +++ b/contrib/navigation/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# 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('navigation_navigation', ( + ('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['navigation.Navigation'])), + ('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)), + ('hosting_node', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='hosted_navigation_set', null=True, to=orm['philo.Node'])), + ('target_node', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='targeting_navigation_set', 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)), + ('text', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)), + ('order', self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, blank=True)), + ('depth', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=3, null=True, blank=True)), + )) + db.send_create_signal('navigation', ['Navigation']) + + + def backwards(self, orm): + + # Deleting model 'Navigation' + db.delete_table('navigation_navigation') + + + 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'}) + }, + 'navigation.navigation': { + 'Meta': {'object_name': 'Navigation'}, + 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3', 'null': 'True', 'blank': 'True'}), + 'hosting_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'hosted_navigation_set'", 'null': 'True', 'to': "orm['philo.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'}), + 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['navigation.Navigation']"}), + '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': "'targeting_navigation_set'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) + }, + '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', [], {}) + } + } + + complete_apps = ['navigation'] diff --git a/contrib/navigation/migrations/0002_auto__chg_field_navigation_text.py b/contrib/navigation/migrations/0002_auto__chg_field_navigation_text.py new file mode 100644 index 0000000..c7f4189 --- /dev/null +++ b/contrib/navigation/migrations/0002_auto__chg_field_navigation_text.py @@ -0,0 +1,69 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Navigation.text' + db.alter_column('navigation_navigation', 'text', self.gf('django.db.models.fields.CharField')(max_length=50)) + + + def backwards(self, orm): + + # Changing field 'Navigation.text' + db.alter_column('navigation_navigation', 'text', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)) + + + 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'}) + }, + 'navigation.navigation': { + 'Meta': {'object_name': 'Navigation'}, + 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3', 'null': 'True', 'blank': 'True'}), + 'hosting_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'hosted_navigation_set'", 'null': 'True', 'to': "orm['philo.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'}), + 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['navigation.Navigation']"}), + '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': "'targeting_navigation_set'", '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'}) + }, + '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', [], {}) + } + } + + complete_apps = ['navigation'] diff --git a/contrib/navigation/migrations/__init__.py b/contrib/navigation/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/navigation/models.py b/contrib/navigation/models.py new file mode 100644 index 0000000..f7f0f56 --- /dev/null +++ b/contrib/navigation/models.py @@ -0,0 +1,139 @@ +#encoding: utf-8 +from django.core.exceptions import ValidationError +from django.core.urlresolvers import NoReverseMatch +from django.db import models +from philo.models import TreeEntity, JSONField, Node +from philo.validators import RedirectValidator + +#from mptt.templatetags.mptt_tags import cache_tree_children + + +DEFAULT_NAVIGATION_DEPTH = 3 + + +class NavigationManager(models.Manager): + + # Analagous to contenttypes, cache Navigation to avoid repeated lookups all over the place. + # Navigation will probably be used frequently. + _cache = {} + + def for_node(self, node): + """ + Returns the set of Navigation objects for a given node's navigation. This + will be the most recent set of defined hosted navigation among the node's + ancestors. Lookups are cached so that subsequent lookups for the same node + don't hit the database. + + TODO: Should this create the auto-generated navigation in "physical" form? + """ + key = node.pk + try: + hosted_navigation = self.__class__._cache[self.db][key] + except KeyError: + # Find the most recent host! + ancestors = node.get_ancestors(ascending=True, include_self=True).annotate(num_navigation=models.Count("hosted_navigation_set")) + + # Iterate down the ancestors until you find one that: + # a) is cached, or + # b) has hosted navigation. + pks_to_cache = [] + host_node = None + for ancestor in ancestors: + if ancestor.pk in self.__class__._cache[self.db] or ancestor.num_navigation > 0: + host_node = ancestor + break + else: + pks_to_cache.append(ancestor.pk) + + if host_node is None: + return self.none() + + if ancestor.pk not in self.__class__._cache[self.db]: + self.__class__._cache[self.db][ancestor.pk] = host_node.hosted_navigation_set.select_related('target_node') + + hosted_navigation = self.__class__._cache[self.db][ancestor.pk] + + # Cache the queryset instance for every pk that was passed over, as well. + for pk in pks_to_cache: + self.__class__._cache[self.db][pk] = hosted_navigation + + return hosted_navigation + + def clear_cache(self, navigation=None): + """ + Clear out the navigation cache. This needs to happen during database flushes + or if a navigation entry is changed to prevent caching of outdated navigation information. + + TODO: call this method from update() and delete()! + """ + if navigation is None: + self.__class__._cache.clear() + else: + cache = self.__class__._cache[self.db] + for pk in cache.keys(): + for qs in cache[pk]: + if navigation in qs: + cache.pop(pk) + break + else: + for instance in qs: + if navigation.is_descendant(instance): + cache.pop(pk) + break + # necessary? + if pk not in cache: + break + + +class Navigation(TreeEntity): + text = models.CharField(max_length=50) + + hosting_node = models.ForeignKey(Node, blank=True, null=True, related_name='hosted_navigation_set', help_text="Be part of this node's root navigation.") + + target_node = models.ForeignKey(Node, blank=True, null=True, related_name='targeting_navigation_set', 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(blank=True, null=True) + depth = models.PositiveSmallIntegerField(blank=True, null=True, default=DEFAULT_NAVIGATION_DEPTH, help_text="For the root of a hosted tree, defines the depth of the tree. A blank depth will hide this section of navigation. Otherwise, depth is ignored.") + + 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) + + 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: + view_name = self.url_or_subpath + params = self.reversing_parameters + args = isinstance(params, list) and params or None + kwargs = isinstance(params, dict) and params or None + return node.view.reverse(view_name, args=args, kwargs=kwargs, node=node) + else: + subpath = self.url_or_subpath + while subpath and subpath[0] == '/': + subpath = subpath[1:] + return '%s%s' % (node.get_absolute_url(), subpath) + elif node is not None: + return node.get_absolute_url() + else: + return self.url_or_subpath + + def __unicode__(self): + return self.get_path(field='text', pathsep=u' › ') + + # TODO: Add delete and save methods to handle cache clearing. + + class Meta: + ordering = ['order'] + verbose_name_plural = 'navigation' \ No newline at end of file diff --git a/forms.py b/forms.py index b404f0d..a1785fb 100644 --- a/forms.py +++ b/forms.py @@ -9,7 +9,7 @@ from django.forms.formsets import TOTAL_FORM_COUNT from django.template import loader, loader_tags, TemplateDoesNotExist, Context, Template as DjangoTemplate from django.utils.datastructures import SortedDict from philo.admin.widgets import ModelLookupWidget -from philo.models import Entity, Template, Contentlet, ContentReference, Attribute, Node, NodeNavigationOverride +from philo.models import Entity, Template, Contentlet, ContentReference, Attribute from philo.utils import fattr @@ -337,57 +337,4 @@ class ContentReferenceInlineFormSet(ContainerInlineFormSet): name, content_type = self.extra_containers[i - self.initial_form_count() - 1] kwargs['instance'] = self.model(name=name, content_type=content_type) - return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs) - - -class NodeWithOverrideForm(forms.ModelForm): - title = NodeNavigationOverride._meta.get_field('title').formfield() - url = NodeNavigationOverride._meta.get_field('url').formfield() - child_navigation = NodeNavigationOverride._meta.get_field('child_navigation').formfield(required=False) - - def __init__(self, *args, **kwargs): - super(NodeWithOverrideForm, self).__init__(*args, **kwargs) - if self.instance.pk: - self._override = override = self.get_override(self.instance) - self.initial.update({ - 'title': override.title, - 'url': override.url, - 'child_navigation': override.child_navigation_json - }) - - def get_override(self, instance): - try: - return NodeNavigationOverride.objects.get(parent=self.instance.parent, child=self.instance) - except NodeNavigationOverride.DoesNotExist: - override = NodeNavigationOverride(parent=self.instance.parent, child=self.instance) - override.child_navigation = None - return override - - def save(self, commit=True): - obj = super(NodeWithOverrideForm, self).save(commit) - cleaned_data = self.cleaned_data - override = self.get_override(obj) - - # Override information should only be set if there was no previous override or if the - # information was just manually set - i.e. was not equal to the data on the cached override. - if not override.pk or cleaned_data['title'] != self._override.title or cleaned_data['url'] != self._override.url or cleaned_data['child_navigation'] != self._override.child_navigation: - override.title = self.cleaned_data['title'] - override.url = self.cleaned_data['url'] - override.child_navigation = self.cleaned_data['child_navigation'] - override.save() - return obj - - class Meta: - model = Node - - -class NodeOverrideInlineFormSet(BaseInlineFormSet): - def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): - if queryset is None: - queryset = self.model._default_manager - queryset = queryset.filter(parent=instance, child__parent=instance) - super(NodeOverrideInlineFormSet, self).__init__(data, files, instance, save_as_new, prefix, queryset) - - def add_fields(self, form, index): - super(NodeOverrideInlineFormSet, self).add_fields(form, index) - form.fields['child'].queryset = self.instance.children.all() \ No newline at end of file + return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs) \ No newline at end of file diff --git a/migrations/0010_auto__add_nodenavigationoverride.py b/migrations/0010_auto__add_nodenavigationoverride.py deleted file mode 100644 index e36b98c..0000000 --- a/migrations/0010_auto__add_nodenavigationoverride.py +++ /dev/null @@ -1,157 +0,0 @@ -# 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 'NodeNavigationOverride' - db.create_table('philo_nodenavigationoverride', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, null=True, related_name='child_navigation_overrides', to=orm['philo.Node'])), - ('child', self.gf('django.db.models.fields.related.ForeignKey')(related_name='navigation_overrides', to=orm['philo.Node'])), - ('title', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), - ('url', self.gf('django.db.models.fields.CharField')(max_length=200, blank=True)), - ('order', self.gf('django.db.models.fields.PositiveSmallIntegerField')(null=True, blank=True)), - ('child_navigation', self.gf('philo.models.fields.JSONField')(default=None)), - ('hide', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), - )) - db.send_create_signal('philo', ['NodeNavigationOverride']) - - - def backwards(self, orm): - - # Deleting model 'NodeNavigationOverride' - db.delete_table('philo_nodenavigationoverride') - - - 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', [], {}) - }, - '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.nodenavigationoverride': { - 'Meta': {'unique_together': "(('parent', 'child'),)", 'object_name': 'NodeNavigationOverride'}, - 'child': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_overrides'", 'to': "orm['philo.Node']"}), - 'child_navigation': ('philo.models.fields.JSONField', [], {'default': 'None'}), - 'hide': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'child_navigation_overrides'", 'to': "orm['philo.Node']"}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'url': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'philo.page': { - 'Meta': {'object_name': 'Page'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'philo.redirect': { - 'Meta': {'object_name': 'Redirect'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}), - 'target': ('django.db.models.fields.CharField', [], {'max_length': '200'}) - }, - '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 5676b58..de10ed1 100644 --- a/models/nodes.py +++ b/models/nodes.py @@ -10,16 +10,13 @@ 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 from philo.signals import view_about_to_render, view_finished_rendering -from mptt.templatetags.mptt_tags import cache_tree_children _view_content_type_limiter = ContentTypeSubclassLimiter(None) -DEFAULT_NAVIGATION_DEPTH = 3 class Node(TreeEntity): @@ -51,46 +48,6 @@ class Node(TreeEntity): except AncestorDoesNotExist, ViewDoesNotExist: return None - def get_navigation(self, depth=DEFAULT_NAVIGATION_DEPTH): - max_depth = depth + self.get_level() - tree = cache_tree_children(self.get_descendants(include_self=True).filter(level__lte=max_depth)) - - def get_nav(parent, nodes): - node_overrides = dict([(override.child.pk, override) for override in NodeNavigationOverride.objects.filter(parent=parent, child__in=nodes).select_related('child')]) - - navigation_list = [] - for node in nodes: - node._override = node_overrides.get(node.pk, None) - - if node._override: - if node._override.hide: - continue - navigation = node._override.get_navigation(node, max_depth) - else: - navigation = node.view.get_navigation(node, max_depth) - - if not node.is_leaf_node() and node.get_level() < max_depth: - children = navigation.get('children', []) - children += get_nav(node, node.get_children()) - navigation['children'] = children - - if 'children' in navigation: - navigation['children'].sort(cmp=lambda x,y: cmp(x['order'], y['order'])) - - navigation_list.append(navigation) - - return navigation_list - - navigation = get_nav(self.parent, tree) - root = navigation[0] - navigation = [root] + root['children'] - del(root['children']) - return navigation - - def save(self): - super(Node, self).save() - - class Meta: app_label = 'philo' @@ -99,47 +56,6 @@ class Node(TreeEntity): models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node') -class NodeNavigationOverride(Entity): - parent = models.ForeignKey(Node, related_name="child_navigation_overrides", blank=True, null=True) - child = models.ForeignKey(Node, related_name="navigation_overrides") - - title = models.CharField(max_length=100, blank=True) - url = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True) - order = models.PositiveSmallIntegerField(blank=True, null=True) - child_navigation = JSONField() - hide = models.BooleanField() - - def get_navigation(self, node, max_depth): - default = node.view.get_navigation(node, max_depth) - if self.url: - default['url'] = self.url - if self.title: - default['title'] = self.title - if self.order: - default['order'] = self.order - if isinstance(self.child_navigation, list) and node.get_level() < max_depth: - child_navigation = self.child_navigation[:] - - for child in child_navigation: - child['url'] = default['url'] + child['url'] - - if 'children' in default: - overridden = set([child['url'] for child in default['children']]) & set([child['url'] for child in self.child_navigation]) - if overridden: - for child in default[:]: - if child['url'] in overridden: - default.remove(child) - default['children'] += self.child_navigation - else: - default['children'] = self.child_navigation - return default - - class Meta: - ordering = ['order'] - unique_together = ('parent', 'child',) - app_label = 'philo' - - class View(Entity): nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id') @@ -175,22 +91,6 @@ class View(Entity): def actually_render_to_response(self, request, extra_context=None): raise NotImplementedError('View subclasses must implement render_to_response.') - def get_navigation(self, node, max_depth): - """ - Subclasses should implement get_navigation to support auto-generated navigation. - max_depth is the deepest `level` that should be generated; node is the node that - is asking for the navigation. This method should return a dictionary of the form: - { - 'url': url, - 'title': title, - 'order': order, # None for no ordering. - 'children': [ # Optional - - ] - } - """ - raise NotImplementedError('View subclasses must implement get_navigation.') - class Meta: abstract = True diff --git a/models/pages.py b/models/pages.py index 6f5bc9a..81b84c9 100644 --- a/models/pages.py +++ b/models/pages.py @@ -102,13 +102,6 @@ class Page(View): if errors: raise ValidationError(errors) - def get_navigation(self, node, max_depth): - return { - 'url': node.get_absolute_url(), - 'title': self.title, - 'order': None, - } - class Meta: app_label = 'philo' diff --git a/templatetags/nodes.py b/templatetags/nodes.py index c2dcd9a..73492d4 100644 --- a/templatetags/nodes.py +++ b/templatetags/nodes.py @@ -114,84 +114,4 @@ def do_node_url(parser, token): args.append(parser.compile_filter(value)) return NodeURLNode(view_name=view_name, args=args, kwargs=kwargs, node=node, as_var=as_var) - return NodeURLNode(node=node, as_var=as_var) - - -class NavigationNode(template.Node): - def __init__(self, node=None, as_var=None): - self.as_var = as_var - self.node = node - - def render(self, context): - if 'request' not in context: - return settings.TEMPLATE_STRING_IF_INVALID - - if self.node: - node = self.node.resolve(context) - else: - node = context.get('node', None) - - if not node: - return settings.TEMPLATE_STRING_IF_INVALID - - try: - nav_root = node.attributes['navigation_root'] - except KeyError: - if settings.TEMPLATE_DEBUG: - raise - return settings.TEMPLATE_STRING_IF_INVALID - - # Should I get its override and check for a max depth override there? - navigation = nav_root.get_navigation() - - if self.as_var: - context[self.as_var] = navigation - return '' - - return self.compile(navigation, context['request'].path, nav_root.get_absolute_url(), nav_root.get_level(), nav_root.get_level() + 3) - - def compile(self, navigation, active_path, root_url, current_depth, max_depth): - compiled = "" - for item in navigation: - if item['url'] in active_path and (item['url'] != root_url or root_url == active_path): - compiled += "
  • " - else: - compiled += "
  • " - - if item['url']: - compiled += "" % item['url'] - - compiled += item['title'] - - if item['url']: - compiled += "" - - if 'children' in item and current_depth < max_depth: - compiled += "
      %s
    " % self.compile(item['children'], active_path, root_url, current_depth + 1, max_depth) - - compiled += "
  • " - return compiled - - -@register.tag(name='navigation') -def do_navigation(parser, token): - """ - {% navigation [for ] [as ] %} - """ - bits = token.split_contents() - tag = bits[0] - bits = bits[1:] - node = None - as_var = None - - if len(bits) >= 2 and bits[-2] == 'as': - as_var = bits[-1] - bits = bits[:-2] - - if len(bits) >= 2 and bits[-2] == 'for': - node = parser.compile_filter(bits[-1]) - bits = bits[-2] - - if bits: - raise template.TemplateSyntaxError('`%s` template tag expects the syntax {%% %s [for ] [as ] %}' % (tag, tag)) - return NavigationNode(node, as_var) + return NodeURLNode(node=node, as_var=as_var) \ No newline at end of file -- 2.20.1