From: Stephen Burrows Date: Tue, 21 Dec 2010 15:12:03 +0000 (-0500) Subject: Merge branch 'master' into navigation X-Git-Tag: philo-0.9~22^2~17^2~8 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/64e4cdefe89f642d349b5908a3bbaec76333e3b2?hp=631688575ca2c0a4bdfdf99bdd2470ee9630bc25 Merge branch 'master' into navigation --- diff --git a/admin/nodes.py b/admin/nodes.py index 45a3172..0fac7ad 100644 --- a/admin/nodes.py +++ b/admin/nodes.py @@ -1,10 +1,32 @@ from django.contrib import admin -from philo.admin.base import EntityAdmin, TreeEntityAdmin -from philo.models import Node, Redirect, File +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 class NodeAdmin(TreeEntityAdmin): - pass + 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 class ViewAdmin(EntityAdmin): diff --git a/forms.py b/forms.py index a1785fb..b404f0d 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 +from philo.models import Entity, Template, Contentlet, ContentReference, Attribute, Node, NodeNavigationOverride from philo.utils import fattr @@ -337,4 +337,57 @@ 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) \ No newline at end of file + 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 diff --git a/migrations/0010_auto__add_nodenavigationoverride.py b/migrations/0010_auto__add_nodenavigationoverride.py new file mode 100644 index 0000000..e36b98c --- /dev/null +++ b/migrations/0010_auto__add_nodenavigationoverride.py @@ -0,0 +1,157 @@ +# 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 de10ed1..5676b58 100644 --- a/models/nodes.py +++ b/models/nodes.py @@ -10,13 +10,16 @@ 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): @@ -48,6 +51,46 @@ 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' @@ -56,6 +99,47 @@ 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') @@ -91,6 +175,22 @@ 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 81b84c9..6f5bc9a 100644 --- a/models/pages.py +++ b/models/pages.py @@ -102,6 +102,13 @@ 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 73492d4..c2dcd9a 100644 --- a/templatetags/nodes.py +++ b/templatetags/nodes.py @@ -114,4 +114,84 @@ 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) \ No newline at end of file + 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)