Merge branch 'release' into develop
authorStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 1 Jun 2011 21:50:47 +0000 (17:50 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 1 Jun 2011 21:50:47 +0000 (17:50 -0400)
23 files changed:
docs/index.rst
docs/intro.rst [deleted file]
docs/models/entities.rst
docs/tutorials/getting-started.rst [new file with mode: 0644]
docs/tutorials/intro.rst [new file with mode: 0644]
docs/what.rst [new file with mode: 0644]
philo/admin/base.py
philo/admin/forms/containers.py
philo/admin/pages.py
philo/admin/widgets.py
philo/contrib/penfield/models.py
philo/contrib/shipherd/migrations/0003_auto__del_field_navigationitem_slug.py [new file with mode: 0644]
philo/contrib/shipherd/models.py
philo/fixtures/test_fixtures.json
philo/middleware.py
philo/migrations/0016_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py [new file with mode: 0644]
philo/models/base.py
philo/models/nodes.py
philo/models/pages.py
philo/static/philo/js/TagCreation.js [moved from philo/static/admin/js/TagCreation.js with 91% similarity]
philo/tests.py
philo/utils/lazycompat.py [new file with mode: 0644]
philo/views.py

index 666e3ee..079185d 100644 (file)
@@ -8,12 +8,27 @@
 Welcome to Philo's documentation!
 =================================
 
-Contents:
+Philo is a foundation for developing web content management systems.
+
+Prerequisites:
+
+* `Python 2.5.4+ <http://www.python.org>`_
+* `Django 1.2+ <http://www.djangoproject.com/>`_
+* `django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>`_
+* (Optional) `django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>`_
+* (Optional) `south 0.7.2+ <http://south.aeracode.org/>`_
+* (Optional) `recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>`_
+
+To contribute, please visit the `project website <http://philocms.org/>`_ or make a fork of the git repository on `GitHub <http://github.com/ithinksw/philo>`_ or `Gitorious <http://gitorious.org/ithinksw/philo>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
+
+Contents
+++++++++
 
 .. toctree::
-       :maxdepth: 2
+       :maxdepth: 1
        
-       intro
+       what
+       tutorials/intro
        models/intro
        exceptions
        handling_requests
@@ -26,24 +41,8 @@ Contents:
        contrib/intro
 
 Indices and tables
-==================
+++++++++++++++++++
 
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
-What is Philo?
-==============
-
-Philo is a foundation for developing web content management systems.
-
-Prerequisites:
-
-* `Python 2.5.4+ <http://www.python.org>`_
-* `Django 1.2+ <http://www.djangoproject.com/>`_
-* `django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>`_
-* (Optional) `django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>`_
-* (Optional) `south 0.7.2+ <http://south.aeracode.org/>`_
-* (Optional) `recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>`_
-
-To contribute, please visit the `project website <http://philocms.org/>`_ or make a fork of the git repository on `GitHub <http://github.com/ithinksw/philo>`_ or `Gitorious <http://gitorious.org/ithinksw/philo>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
diff --git a/docs/intro.rst b/docs/intro.rst
deleted file mode 100644 (file)
index 2b253db..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-How to get started with philo
-=============================
-
-After installing `philo`_ and `mptt`_ on your python path, make sure to complete the following steps:
-
-1. add :mod:`philo` and :mod:`mptt` to :setting:`settings.INSTALLED_APPS`::
-       
-       INSTALLED_APPS = (
-               ...
-               'philo',
-               'mptt',
-               ...
-       )
-       
-2. add :class:`philo.middleware.RequestNodeMiddleware` to :setting:`settings.MIDDLEWARE_CLASSES`::
-       
-       MIDDLEWARE_CLASSES = (
-               ...
-               'philo.middleware.RequestNodeMiddleware',
-               ...
-       )
-       
-3. include :mod:`philo.urls` somewhere in your urls.py file. For example::
-       
-       from django.conf.urls.defaults import patterns, include, url
-       urlpatterns = patterns('',
-               url(r'^', include('philo.urls')),
-       )
-       
-4. Optionally add a root :class:`node <philo.models.Node>` to your current :class:`Site` in the admin interface.
-
-Philo should be ready to go!
-
-.. _philo: http://philocms.org/
-.. _mptt: http://github.com/django-mptt/django-mptt
index a394c6f..b39a253 100644 (file)
@@ -41,7 +41,7 @@ Entities
 .. autoclass:: Entity
        :members:
 
-.. autoclass:: TreeManager
+.. autoclass:: TreeEntityManager
        :members:
 
 .. autoclass:: TreeEntity
@@ -50,6 +50,6 @@ Entities
 
        .. attribute:: objects
 
-               An instance of :class:`TreeManager`.
+               An instance of :class:`TreeEntityManager`.
        
        .. automethod:: get_path
\ No newline at end of file
diff --git a/docs/tutorials/getting-started.rst b/docs/tutorials/getting-started.rst
new file mode 100644 (file)
index 0000000..eeb9ce8
--- /dev/null
@@ -0,0 +1,87 @@
+Getting started with philo
+==========================
+
+.. note:: This guide assumes that you have worked with Django's built-in administrative interface.
+
+Once you've installed `philo`_ and `mptt`_ to your python path, there are only a few things that you need to do to get :mod:`philo` working.
+
+1. Add :mod:`philo` and :mod:`mptt` to :setting:`settings.INSTALLED_APPS`::
+               
+       INSTALLED_APPS = (
+               ...
+               'philo',
+               'mptt',
+               ...
+       )
+
+2. Syncdb or run migrations to set up your database.
+       
+3. Add :class:`philo.middleware.RequestNodeMiddleware` to :setting:`settings.MIDDLEWARE_CLASSES`::
+       
+       MIDDLEWARE_CLASSES = (
+               ...
+               'philo.middleware.RequestNodeMiddleware',
+               ...
+       )
+       
+4. Include :mod:`philo.urls` somewhere in your urls.py file. For example::
+       
+       from django.conf.urls.defaults import patterns, include, url
+       urlpatterns = patterns('',
+               url(r'^', include('philo.urls')),
+       )
+
+Philo should be ready to go! (Almost.)
+
+.. _philo: http://philocms.org/
+.. _mptt: http://github.com/django-mptt/django-mptt
+
+Hello world
++++++++++++
+
+Now that you've got everything configured, it's time to set up your first page! Easy peasy. Open up the admin and add a new :class:`.Template`. Call it "Hello World Template". The code can be something like this::
+       
+       <html>
+               <head>
+                       <title>Hello world!</title>
+               </head>
+               <body>
+                       <p>Hello world!</p>
+                       <p>The time is {% now %}.</p>
+               </body>
+       </html>
+
+Next, add a philo :class:`.Page` - let's call it "Hello World Page" and use the template you just made.
+
+Now make a philo :class:`.Node`. Give it the slug ``hello-world``. Set the view content type to "Page" and use the page that you just made. If you navigate to ``/hello-world``, you will see the results of rendering the page!
+
+Setting the root node
++++++++++++++++++++++
+
+So what's at ``/``? If you try to load it, you'll get a 404 error. This is because there's no :class:`.Node` located there - and since :attr:`.Node.slug` is a required field, getting a node there is not as simple as leaving the :attr:`.~Node.slug` blank.
+
+In :mod:`philo`, the node that is displayed at ``/`` is called the "root node" of the current :class:`Site`. To represent this idea cleanly in the database, :mod:`philo` adds a :class:`ForeignKey` to :class:`.Node` to the :class:`django.contrib.sites.models.Site` model.
+
+Since there's only one :class:`.Node` in your :class:`Site`, we probably want ``hello-world`` to be the root node. All you have to do is edit the current :class:`Site` and set its root node to ``hello-world``. Now you can see the page rendered at ``/``!
+
+Editing page contents
++++++++++++++++++++++
+
+Great! We've got a page that says "Hello World". But what if we want it to say something else? Should we really have to edit the :class:`.Template` to change the content of the :class:`.Page`? And what if we want to share the :class:`.Template` but have different content? Adjust the :class:`.Template` to look like this::
+       
+       <html>
+               <head>
+                       <title>{% container page_title %}</title>
+               </head>
+               <body>
+                       {% container page_body as content %}
+                       {% if content %}
+                               <p>{{ content }}</p>
+                       {% endif %}
+                       <p>The time is {% now %}.</p>
+               </body>
+       </html>
+
+Now go edit your :class:`.Page`. Two new fields called "Page title" and "Page body" have shown up! You can put anything you like in here and have it show up in the appropriate places when the page is rendered.
+
+.. seealso:: :ttag:`philo.templatetags.containers.container`
diff --git a/docs/tutorials/intro.rst b/docs/tutorials/intro.rst
new file mode 100644 (file)
index 0000000..903dfea
--- /dev/null
@@ -0,0 +1,7 @@
+Tutorials
+=========
+
+.. toctree::
+       :maxdepth: 1
+       
+       getting-started
diff --git a/docs/what.rst b/docs/what.rst
new file mode 100644 (file)
index 0000000..ac44619
--- /dev/null
@@ -0,0 +1,11 @@
+What is Philo, anyway?
+======================
+
+Philo allows the creation of site structures using Django's built-in admin interface. Like Django, Philo separates URL structure from backend code from display:
+
+* :class:`.Node`\ s represent the URL hierarchy of the website.
+* :class:`.View`\ s contain the logic for each :class:`.Node`, as simple as a :class:`.Redirect` or as complex as a :class:`.Blog`.
+* :class:`.Page`\ s (the most commonly used :class:`.View`) render whatever context they are passed using database-driven :class:`.Template`\ s written with Django's template language.
+* :class:`.Attribute`\ s are arbitrary key/value pairs which can be attached to most of the models that Philo provides. Attributes of a :class:`.Node` will be inherited by all of the :class:`.Node`'s descendants and will be available in the template's context.
+
+The :ttag:`~philo.templatetags.containers.container` template tag that Philo provides makes it easy to mark areas in a template which need to be editable page-by-page; every :class:`.Page` will have an additional field in the admin for each :ttag:`~philo.templatetags.containers.container` in the template it uses.
index 3a9458e..81916ab 100644 (file)
@@ -136,11 +136,7 @@ class EntityAdmin(admin.ModelAdmin):
                return db_field.formfield(**kwargs)
 
 
-class TreeAdmin(MPTTModelAdmin):
-       pass
-
-
-class TreeEntityAdmin(EntityAdmin, TreeAdmin):
+class TreeEntityAdmin(EntityAdmin, MPTTModelAdmin):
        pass
 
 
index 246a954..987524f 100644 (file)
@@ -153,7 +153,7 @@ class ContainerInlineFormSet(BaseInlineFormSet):
 class ContentletInlineFormSet(ContainerInlineFormSet):
        def get_containers(self):
                try:
-                       containers = list(self.instance.containers[0])
+                       containers = self.instance.containers[0]
                except ObjectDoesNotExist:
                        containers = []
                
index fd8665b..3e8f0f1 100644 (file)
@@ -2,7 +2,7 @@ from django import forms
 from django.conf import settings
 from django.contrib import admin
 
-from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
+from philo.admin.base import COLLAPSE_CLASSES, TreeEntityAdmin
 from philo.admin.forms.containers import *
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
@@ -55,7 +55,7 @@ class PageAdmin(ViewAdmin):
                return super(PageAdmin, self).response_add(request, obj, post_url_continue)
 
 
-class TemplateAdmin(TreeAdmin):
+class TemplateAdmin(TreeEntityAdmin):
        prepopulated_fields = {'slug': ('name',)}
        fieldsets = (
                (None, {
index 62a492b..c753850 100644 (file)
@@ -52,7 +52,7 @@ class TagFilteredSelectMultiple(FilteredSelectMultiple):
                        settings.ADMIN_MEDIA_PREFIX + "js/core.js",
                        settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
                        settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js",
-                       settings.ADMIN_MEDIA_PREFIX + "js/TagCreation.js",
+                       "philo/js/TagCreation.js",
                )
 
        def render(self, name, value, attrs=None, choices=()):
index b8ca610..2a145fb 100644 (file)
@@ -51,9 +51,9 @@ class FeedView(MultiView):
        #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15.
        feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
        
-       #: A :class:`ForeignKey` to a :class:`.Template` which can be used to render the title of each item in the feed.
+       #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
        item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
-       #: A :class:`ForeignKey` to a :class:`.Template` which can be used to render the description of each item in the feed.
+       #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
        item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
        
        #: The name of the context variable to be populated with the items managed by the :class:`FeedView`.
@@ -201,7 +201,7 @@ class FeedView(MultiView):
                        language = settings.LANGUAGE_CODE.decode(),
                        feed_url = add_domain(
                                current_site.domain,
-                               self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()),
+                               self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_secure()),
                                request.is_secure()
                        ),
                        author_name = self.__get_dynamic_attr('author_name', obj),
diff --git a/philo/contrib/shipherd/migrations/0003_auto__del_field_navigationitem_slug.py b/philo/contrib/shipherd/migrations/0003_auto__del_field_navigationitem_slug.py
new file mode 100644 (file)
index 0000000..5d7d5e3
--- /dev/null
@@ -0,0 +1,74 @@
+# 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 'NavigationItem.slug'
+        db.delete_column('shipherd_navigationitem', 'slug')
+
+
+    def backwards(self, orm):
+        
+        # User chose to not deal with backwards NULL issues for 'NavigationItem.slug'
+        raise RuntimeError("Cannot reverse this migration. 'NavigationItem.slug' and its values cannot be restored.")
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'philo.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', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        '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', 'db_index': 'True'}),
+            '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'}),
+            'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shipherd_navigationitem_related'", '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']
index a520fbe..429faaa 100644 (file)
@@ -7,7 +7,7 @@ from django.core.validators import RegexValidator, MinValueValidator
 from django.db import models
 from django.forms.models import model_to_dict
 
-from philo.models.base import TreeEntity, TreeManager, Entity
+from philo.models.base import TreeEntity, TreeEntityManager, Entity
 from philo.models.nodes import Node, TargetURLModel
 
 
@@ -224,7 +224,7 @@ class Navigation(Entity):
                unique_together = ('node', 'key')
 
 
-class NavigationItemManager(TreeManager):
+class NavigationItemManager(TreeEntityManager):
        use_for_related = True
        
        def get_query_set(self):
@@ -249,8 +249,9 @@ class NavigationItem(TreeEntity, TargetURLModel):
                self._initial_data = model_to_dict(self)
                self._is_cached = False
        
-       def __unicode__(self):
-               return self.get_path(field='text', pathsep=u' â€º ')
+       def get_path(self, root=None, pathsep=u' â€º ', field='text'):
+               return super(NavigationItem, self).get_path(root, pathsep, field)
+       path = property(get_path)
        
        def clean(self):
                super(NavigationItem, self).clean()
index 4c55372..2bda0d1 100644 (file)
@@ -91,8 +91,8 @@
             "rght": 143, 
             "view_object_id": 1, 
             "view_content_type": [
-                "penfield", 
-                "blogview"
+                "philo", 
+                "page"
             ], 
             "parent": 1, 
             "level": 1, 
         "model": "philo.redirect", 
         "fields": {
             "status_code": 302, 
-            "target": "second"
+            "url_or_subpath": "second"
         }
     }, 
     {
             "template": 6, 
             "title": "Tag Archive Page"
         }
-    }, 
-    {
-        "pk": 1, 
-        "model": "penfield.blog", 
-        "fields": {
-            "slug": "free-lovin", 
-            "title": "Free lovin'"
-        }
-    }, 
-    {
-        "pk": 1, 
-        "model": "penfield.blogentry", 
-        "fields": {
-            "content": "Lorem ipsum.\r\n\r\nDolor sit amet.", 
-            "author": 1, 
-            "title": "First Entry", 
-            "excerpt": "", 
-            "blog": 1, 
-            "date": "2010-10-20 10:38:58", 
-            "slug": "first-entry", 
-            "tags": [
-                1
-            ]
-        }
-    }, 
-    {
-        "pk": 1, 
-        "model": "penfield.blogview", 
-        "fields": {
-            "entry_archive_page": 5, 
-            "tag_page": 4, 
-            "feed_suffix": "feed", 
-            "entry_permalink_style": "D", 
-            "tag_permalink_base": "tags", 
-            "feeds_enabled": true, 
-            "entries_per_page": null, 
-            "tag_archive_page": 6, 
-            "blog": 1, 
-            "entry_permalink_base": "entries", 
-            "index_page": 2, 
-            "entry_page": 3
-        }
     }
 ]
index b90067a..037fdc8 100644 (file)
@@ -3,56 +3,47 @@ from django.contrib.sites.models import Site
 from django.http import Http404
 
 from philo.models import Node, View
+from philo.utils.lazycompat import SimpleLazyObject
 
 
-class LazyNode(object):
-       def __get__(self, request, obj_type=None):
-               if not hasattr(request, '_cached_node_path'):
-                       return None
-               
-               if not hasattr(request, '_found_node'):
-                       try:
-                               current_site = Site.objects.get_current()
-                       except Site.DoesNotExist:
-                               current_site = None
-                       
-                       path = request._cached_node_path
-                       trailing_slash = False
-                       if path[-1] == '/':
-                               trailing_slash = True
-                       
-                       try:
-                               node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False)
-                       except Node.DoesNotExist:
-                               node = None
-                       else:
-                               if subpath is None:
-                                       subpath = ""
-                               subpath = "/" + subpath
-                               
-                               if not node.handles_subpath(subpath):
-                                       node = None
-                               else:
-                                       if trailing_slash and subpath[-1] != "/":
-                                               subpath += "/"
-                                       
-                                       node.subpath = subpath
-                       
-                       request._found_node = node
-               
-               return request._found_node
+def get_node(path):
+       """Returns a :class:`Node` instance at ``path`` (relative to the current site) or ``None``."""
+       try:
+               current_site = Site.objects.get_current()
+       except Site.DoesNotExist:
+               current_site = None
+       
+       trailing_slash = False
+       if path[-1] == '/':
+               trailing_slash = True
+       
+       try:
+               node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False)
+       except Node.DoesNotExist:
+               return None
+       
+       if subpath is None:
+               subpath = ""
+       subpath = "/" + subpath
+       
+       if trailing_slash and subpath[-1] != "/":
+               subpath += "/"
+       
+       node._path = path
+       node._subpath = subpath
+       
+       return node
 
 
 class RequestNodeMiddleware(object):
        """Adds a ``node`` attribute, representing the currently-viewed node, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`."""
-       def process_request(self, request):
-               request.__class__.node = LazyNode()
-       
        def process_view(self, request, view_func, view_args, view_kwargs):
                try:
-                       request._cached_node_path = view_kwargs['path']
+                       path = view_kwargs['path']
                except KeyError:
-                       pass
+                       request.node = None
+               else:
+                       request.node = SimpleLazyObject(lambda: get_node(path))
        
        def process_exception(self, request, exception):
                if settings.DEBUG or not hasattr(request, 'node') or not request.node:
diff --git a/philo/migrations/0016_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py b/philo/migrations/0016_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py
new file mode 100644 (file)
index 0000000..7a79fec
--- /dev/null
@@ -0,0 +1,144 @@
+# 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 unique constraint on 'Node', fields ['slug', 'parent']
+        db.create_unique('philo_node', ['slug', 'parent_id'])
+
+        # Adding unique constraint on 'Template', fields ['slug', 'parent']
+        db.create_unique('philo_template', ['slug', 'parent_id'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Template', fields ['slug', 'parent']
+        db.delete_unique('philo_template', ['slug', 'parent_id'])
+
+        # Removing unique constraint on 'Node', fields ['slug', 'parent']
+        db.delete_unique('philo_node', ['slug', 'parent_id'])
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            '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', 'db_index': 'True'}),
+            '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', [], {'db_index': 'True', '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'", 'db_index': 'True'})
+        },
+        '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': {'unique_together': "(('parent', 'slug'),)", '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', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', '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'}),
+            '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': {'ordering': "('name',)", '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': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
index 02d8456..0218261 100644 (file)
@@ -35,18 +35,6 @@ class Tag(models.Model):
                ordering = ('name',)
 
 
-class Titled(models.Model):
-       # Use of this model is deprecated.
-       title = models.CharField(max_length=255)
-       slug = models.SlugField(max_length=255)
-       
-       def __unicode__(self):
-               return self.title
-       
-       class Meta:
-               abstract = True
-
-
 #: An instance of :class:`.ContentTypeRegistryLimiter` which is used to track the content types which can be related to by :class:`ForeignKeyValue`\ s and :class:`ManyToManyValue`\ s.
 value_content_type_limiter = ContentTypeRegistryLimiter()
 
@@ -100,7 +88,7 @@ class AttributeValue(models.Model):
                abstract = True
 
 
-#: An instance of :class:`ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
+#: An instance of :class:`.ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
@@ -246,7 +234,12 @@ class ManyToManyValue(AttributeValue):
 
 
 class Attribute(models.Model):
-       """Represents an arbitrary key/value pair on an arbitrary :class:`Model` where the key consists of word characters and the value is a subclass of :class:`AttributeValue`."""
+       """
+       :class:`Attribute`\ s exist primarily to let arbitrary data be attached to arbitrary model instances without altering the database schema and without guaranteeing that the data will be available on every instance of that model.
+       
+       Generally, :class:`Attribute`\ s will not be accessed as models; instead, they will be accessed through the :attr:`Entity.attributes` property, which allows direct dictionary getting and setting of the value of an :class:`Attribute` with its key.
+       
+       """
        entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
        
@@ -332,10 +325,18 @@ class Entity(models.Model):
                abstract = True
 
 
-class TreeManager(models.Manager):
+class TreeEntityBase(MPTTModelBase, EntityBase):
+       def __new__(meta, name, bases, attrs):
+               attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
+               cls = EntityBase.__new__(meta, name, bases, attrs)
+               
+               return meta.register(cls)
+
+
+class TreeEntityManager(models.Manager):
        use_for_related_fields = True
        
-       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
+       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
                """
                If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises an :class:`~django.core.exceptions.ObjectDoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path).
                
@@ -445,10 +446,12 @@ class TreeManager(models.Manager):
                return find_obj(segments, len(segments)/2 or len(segments))
 
 
-class TreeModel(MPTTModel):
-       objects = TreeManager()
+class TreeEntity(Entity, MPTTModel):
+       """An abstract subclass of Entity which represents a tree relationship."""
+       
+       __metaclass__ = TreeEntityBase
+       objects = TreeEntityManager()
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
-       slug = models.SlugField(max_length=255)
        
        def get_path(self, root=None, pathsep='/', field='slug'):
                """
@@ -462,6 +465,9 @@ class TreeModel(MPTTModel):
                if root == self:
                        return ''
                
+               if root is None and self.is_root_node():
+                       return getattr(self, field, '?')
+               
                if root is not None and not self.is_descendant_of(root):
                        raise AncestorDoesNotExist(root)
                
@@ -473,27 +479,6 @@ class TreeModel(MPTTModel):
                return pathsep.join([getattr(parent, field, '?') for parent in qs])
        path = property(get_path)
        
-       def __unicode__(self):
-               return self.path
-       
-       class Meta:
-               unique_together = (('parent', 'slug'),)
-               abstract = True
-
-
-class TreeEntityBase(MPTTModelBase, EntityBase):
-       def __new__(meta, name, bases, attrs):
-               attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
-               cls = EntityBase.__new__(meta, name, bases, attrs)
-               
-               return meta.register(cls)
-
-
-class TreeEntity(Entity, TreeModel):
-       """An abstract subclass of Entity which represents a tree relationship."""
-       
-       __metaclass__ = TreeEntityBase
-       
        def get_attribute_mapper(self, mapper=None):
                """
                Returns a :class:`.TreeAttributeMapper` or :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly. If an :class:`Attribute` with a given key is not related to the :class:`Entity`, then the mapper will check the parent's attributes.
@@ -517,5 +502,26 @@ class TreeEntity(Entity, TreeModel):
                return super(TreeEntity, self).get_attribute_mapper(mapper)
        attributes = property(get_attribute_mapper)
        
+       def __unicode__(self):
+               return self.path
+       
+       class Meta:
+               abstract = True
+
+
+class SlugTreeEntityManager(TreeEntityManager):
+       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
+               return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
+
+
+class SlugTreeEntity(TreeEntity):
+       objects = SlugTreeEntityManager()
+       slug = models.SlugField(max_length=255)
+       
+       def get_path(self, root=None, pathsep='/', field='slug'):
+               return super(SlugTreeEntity, self).get_path(root, pathsep, field)
+       path = property(get_path)
+       
        class Meta:
+               unique_together = ('parent', 'slug')
                abstract = True
\ No newline at end of file
index 6eeb965..ab3bca5 100644 (file)
@@ -11,7 +11,7 @@ from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedir
 from django.utils.encoding import smart_str
 
 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths
-from philo.models.base import TreeEntity, Entity, register_value_model
+from philo.models.base import SlugTreeEntity, Entity, register_value_model
 from philo.models.fields import JSONField
 from philo.utils import ContentTypeSubclassLimiter
 from philo.utils.entities import LazyPassthroughAttributeMapper
@@ -24,7 +24,7 @@ __all__ = ('Node', 'View', 'MultiView', 'Redirect', 'File')
 _view_content_type_limiter = ContentTypeSubclassLimiter(None)
 
 
-class Node(TreeEntity):
+class Node(SlugTreeEntity):
        """
        :class:`Node`\ s are the basic building blocks of a website using Philo. They define the URL hierarchy and connect each URL to a :class:`View` subclass instance which is used to generate an HttpResponse.
        
@@ -108,7 +108,7 @@ class Node(TreeEntity):
                
                return '%s%s%s%s' % (domain, root_url, path, subpath)
        
-       class Meta:
+       class Meta(SlugTreeEntity.Meta):
                app_label = 'philo'
 
 
@@ -239,7 +239,7 @@ class MultiView(View):
                
                """
                clear_url_caches()
-               subpath = request.node.subpath
+               subpath = request.node._subpath
                view, args, kwargs = resolve(subpath, urlconf=self)
                view_args = getargspec(view)
                if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None):
index bdd9b42..ea3bb64 100644 (file)
@@ -4,6 +4,8 @@
 
 """
 
+import itertools
+
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -14,7 +16,7 @@ from django.template import TemplateDoesNotExist, Context, RequestContext, Templ
 from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
 from django.utils.datastructures import SortedDict
 
-from philo.models.base import TreeModel, register_value_model
+from philo.models.base import SlugTreeEntity, register_value_model
 from philo.models.fields import TemplateField
 from philo.models.nodes import View
 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
@@ -30,7 +32,7 @@ class LazyContainerFinder(object):
        def __init__(self, nodes, extends=False):
                self.nodes = nodes
                self.initialized = False
-               self.contentlet_specs = set()
+               self.contentlet_specs = []
                self.contentreference_specs = SortedDict()
                self.blocks = {}
                self.block_super = False
@@ -47,7 +49,7 @@ class LazyContainerFinder(object):
                        
                        if isinstance(node, ContainerNode):
                                if not node.references:
-                                       self.contentlet_specs.add(node.name)
+                                       self.contentlet_specs.append(node.name)
                                else:
                                        if node.name not in self.contentreference_specs.keys():
                                                self.contentreference_specs[node.name] = node.references
@@ -78,7 +80,27 @@ class LazyContainerFinder(object):
                        self.initialized = True
 
 
-class Template(TreeModel):
+def build_extension_tree(nodelist):
+       nodelists = []
+       extends = None
+       for node in nodelist:
+               if not isinstance(node, TextNode):
+                       if isinstance(node, ExtendsNode):
+                               extends = node
+                       break
+       
+       if extends:
+               if extends.nodelist:
+                       nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
+               loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
+               nodelists.extend(build_extension_tree(loaded_template.nodelist))
+       else:
+               # Base case: root.
+               nodelists.append(LazyContainerFinder(nodelist))
+       return nodelists
+
+
+class Template(SlugTreeEntity):
        """Represents a database-driven django template."""
        #: The name of the template. Used for organization and debugging.
        name = models.CharField(max_length=255)
@@ -97,35 +119,16 @@ class Template(TreeModel):
                """
                template = DjangoTemplate(self.code)
                
-               def build_extension_tree(nodelist):
-                       nodelists = []
-                       extends = None
-                       for node in nodelist:
-                               if not isinstance(node, TextNode):
-                                       if isinstance(node, ExtendsNode):
-                                               extends = node
-                                       break
-                       
-                       if extends:
-                               if extends.nodelist:
-                                       nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
-                               loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
-                               nodelists.extend(build_extension_tree(loaded_template.nodelist))
-                       else:
-                               # Base case: root.
-                               nodelists.append(LazyContainerFinder(nodelist))
-                       return nodelists
-               
                # Build a tree of the templates we're using, placing the root template first.
-               levels = build_extension_tree(template.nodelist)[::-1]
+               levels = build_extension_tree(template.nodelist)
                
-               contentlet_specs = set()
+               contentlet_specs = []
                contentreference_specs = SortedDict()
                blocks = {}
                
-               for level in levels:
+               for level in reversed(levels):
                        level.initialize()
-                       contentlet_specs |= level.contentlet_specs
+                       contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs))
                        contentreference_specs.update(level.contentreference_specs)
                        for name, block in level.blocks.items():
                                if block.block_super:
@@ -136,7 +139,7 @@ class Template(TreeModel):
                for block_list in blocks.values():
                        for block in block_list:
                                block.initialize()
-                               contentlet_specs |= block.contentlet_specs
+                               contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs))
                                contentreference_specs.update(block.contentreference_specs)
                
                return contentlet_specs, contentreference_specs
@@ -145,7 +148,7 @@ class Template(TreeModel):
                """Returns the value of the :attr:`name` field."""
                return self.name
        
-       class Meta:
+       class Meta(SlugTreeEntity.Meta):
                app_label = 'philo'
 
 
similarity index 91%
rename from philo/static/admin/js/TagCreation.js
rename to philo/static/philo/js/TagCreation.js
index d08d41e..610a4f0 100644 (file)
@@ -76,16 +76,23 @@ var tagCreation = window.tagCreation;
                        tagCreation.toggleButton(id);
                        addEvent(input, 'keyup', function() {
                                tagCreation.toggleButton(id);
-                       })
+                       });
                        addEvent(addLink, 'click', function(e) {
                                e.preventDefault();
                                tagCreation.addTagFromSlug(addLink);
+                       });
+                       // SelectFilter actually mistakenly allows submission on enter. We disallow it.
+                       addEvent(input, 'keypress', function(e) {
+                               if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
+                                       e.preventDefault();
+                               }
                        })
                },
                'toggleButton': function(id) {
                        var addLink = tagCreation.cache[id].addLink;
                        var select = $(tagCreation.cache[id].select);
-                       if (select[0].options.length == 0) {
+                       var input = tagCreation.cache[id].input;
+                       if (input.value != "") {
                                if (addLink.style.display == 'none') {
                                        addLink.style.display = 'block';
                                        select.height(select.height() - $(addLink).outerHeight(false))
index a0e0184..15ce752 100644 (file)
@@ -3,15 +3,16 @@ import traceback
 
 from django import template
 from django.conf import settings
-from django.db import connection
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection, models
 from django.template import loader
 from django.template.loaders import cached
 from django.test import TestCase
-from django.test.utils import setup_test_template_loader
+from django.test.utils import setup_test_template_loader, restore_template_loaders
+from django.utils.datastructures import SortedDict
 
-from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
 from philo.exceptions import AncestorDoesNotExist
-from philo.models import Node, Page, Template
+from philo.models import Node, Page, Template, Tag
 
 
 class TemplateTestCase(TestCase):
@@ -56,7 +57,7 @@ class TemplateTestCase(TestCase):
                # Cleanup
                settings.TEMPLATE_DEBUG = old_td
                settings.TEMPLATE_STRING_IF_INVALID = old_invalid
-               loader.template_source_loaders = old_template_loaders
+               restore_template_loaders()
                
                self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-'*70, ("\n%s\n" % ('-'*70)).join(failures)))
        
@@ -64,43 +65,43 @@ class TemplateTestCase(TestCase):
        def get_template_tests(self):
                # SYNTAX --
                # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
-               blog = Blog.objects.all()[0]
+               embedded = Tag.objects.get(pk=1)
                return {
                        # EMBED INCLUSION HANDLING
                        
-                       'embed01': ('{{ embedded.title|safe }}', {'embedded': blog}, blog.title),
-                       'embed02': ('{{ embedded.title|safe }}{{ var1 }}{{ var2 }}', {'embedded': blog}, blog.title),
-                       'embed03': ('{{ embedded.title|safe }} is a lie!', {'embedded': blog}, '%s is a lie!' % blog.title),
+                       'embed01': ('{{ embedded.name|safe }}', {'embedded': embedded}, embedded.name),
+                       'embed02': ('{{ embedded.name|safe }}{{ var1 }}{{ var2 }}', {'embedded': embedded}, embedded.name),
+                       'embed03': ('{{ embedded.name|safe }} is a lie!', {'embedded': embedded}, '%s is a lie!' % embedded.name),
                        
                        # Simple template structure with embed
-                       'simple01': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog 1 %}Simple{% block one %}{% endblock %}', {'blog': blog}, '%sSimple' % blog.title),
-                       'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % blog.title),
-                       'simple03': ('{% embed penfield.blog with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
-                       'simple04': ('{% embed penfield.blog 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
-                       'simple05': ('{% embed penfield.blog with "embed01" %}{% embed blog %}', {'blog': blog}, blog.title),
+                       'simple01': ('{% embed philo.tag with "embed01" %}{% embed philo.tag 1 %}Simple{% block one %}{% endblock %}', {'embedded': embedded}, '%sSimple' % embedded.name),
+                       'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % embedded.name),
+                       'simple03': ('{% embed philo.tag with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+                       'simple04': ('{% embed philo.tag 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+                       'simple05': ('{% embed philo.tag with "embed01" %}{% embed embedded %}', {'embedded': embedded}, embedded.name),
                        
                        # Kwargs
-                       'kwargs01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % blog.title),
+                       'kwargs01': ('{% embed philo.tag with "embed02" %}{% embed philo.tag 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % embedded.name),
                        
                        # Filters/variables
-                       'filters01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % blog.title),
-                       'filters02': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog entry %}', {'entry': 1}, blog.title),
+                       'filters01': ('{% embed philo.tag with "embed02" %}{% embed philo.tag 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % embedded.name),
+                       'filters02': ('{% embed philo.tag with "embed01" %}{% embed philo.tag entry %}', {'entry': 1}, embedded.name),
                        
                        # Blocky structure
                        'block01': ('{% block one %}Hello{% endblock %}', {}, 'Hello'),
-                       'block02': ('{% extends "simple01" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s" % (blog.title, blog.title)),
-                       'block03': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (blog.title, blog.title)),
+                       'block02': ('{% extends "simple01" %}{% block one %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%s" % (embedded.name, embedded.name)),
+                       'block03': ('{% extends "simple01" %}{% embed philo.tag with "embed03" %}{% block one %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (embedded.name, embedded.name)),
                        
                        # Blocks and includes
-                       'block-include01': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% include "simple01" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (blog.title, blog.title, blog.title)),
-                       'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed penfield.blog with "embed03" %}{% include "simple04" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (blog.title, blog.title, blog.title, blog.title)),
+                       'block-include01': ('{% extends "simple01" %}{% embed philo.tag with "embed03" %}{% block one %}{% include "simple01" %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (embedded.name, embedded.name, embedded.name)),
+                       'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed philo.tag with "embed03" %}{% include "simple04" %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (embedded.name, embedded.name, embedded.name, embedded.name)),
                        
                        # Tests for more complex situations...
                        'complex01': ('{% block one %}{% endblock %}complex{% block two %}{% endblock %}', {}, 'complex'),
                        'complex02': ('{% extends "complex01" %}', {}, 'complex'),
-                       'complex03': ('{% extends "complex02" %}{% embed penfield.blog with "embed01" %}', {}, 'complex'),
-                       'complex04': ('{% extends "complex03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, '%scomplex' % blog.title),
-                       'complex05': ('{% extends "complex03" %}{% block one %}{% include "simple04" %}{% endblock %}', {}, '%scomplex' % blog.title),
+                       'complex03': ('{% extends "complex02" %}{% embed philo.tag with "embed01" %}', {}, 'complex'),
+                       'complex04': ('{% extends "complex03" %}{% block one %}{% embed philo.tag 1 %}{% endblock %}', {}, '%scomplex' % embedded.name),
+                       'complex05': ('{% extends "complex03" %}{% block one %}{% include "simple04" %}{% endblock %}', {}, '%scomplex' % embedded.name),
                }
 
 
@@ -110,35 +111,19 @@ class NodeURLTestCase(TestCase):
        fixtures = ['test_fixtures.json']
        
        def setUp(self):
-               if 'south' in settings.INSTALLED_APPS:
-                       from south.management.commands.migrate import Command
-                       command = Command()
-                       command.handle(all_apps=True)
-               
                self.templates = [
-                               ("{% node_url %}", "/root/second/"),
-                               ("{% node_url for node2 %}", "/root/second2/"),
-                               ("{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/second/</p>"),
-                               ("{% node_url for nodes|first %}", "/root/"),
-                               ("{% node_url with entry %}", settings.TEMPLATE_STRING_IF_INVALID),
-                               ("{% node_url with entry for node2 %}", "/root/second2/2010/10/20/first-entry"),
-                               ("{% node_url with tag for node2 %}", "/root/second2/tags/test-tag/"),
-                               ("{% node_url with date for node2 %}", "/root/second2/2010/10/20"),
-                               ("{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}<em>{{ goodbye|upper }}</em>", "<em>/ROOT/SECOND2/2010/10/20</em>"),
-                               ("{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", "/root/second2/2010/10"),
-                               ("{% node_url entries_by_year year=date|date:'Y' for node2 %}", "/root/second2/2010/"),
+                               ("{% node_url %}", "/root/second"),
+                               ("{% node_url for node2 %}", "/root/second2"),
+                               ("{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/second</p>"),
+                               ("{% node_url for nodes|first %}", "/root"),
                ]
                
                nodes = Node.objects.all()
-               blog = Blog.objects.all()[0]
                
                self.context = template.Context({
                        'node': nodes.get(slug='second'),
                        'node2': nodes.get(slug='second2'),
                        'nodes': nodes,
-                       'entry': BlogEntry.objects.all()[0],
-                       'tag': blog.entry_tags.all()[0],
-                       'date': blog.entry_dates['day'][0]
                })
        
        def test_nodeurl(self):
@@ -149,12 +134,6 @@ class TreePathTestCase(TestCase):
        urls = 'philo.urls'
        fixtures = ['test_fixtures.json']
        
-       def setUp(self):
-               if 'south' in settings.INSTALLED_APPS:
-                       from south.management.commands.migrate import Command
-                       command = Command()
-                       command.handle(all_apps=True)
-       
        def assertQueryLimit(self, max, expected_result, *args, **kwargs):
                # As a rough measure of efficiency, limit the number of queries required for a given operation.
                settings.DEBUG = True
@@ -193,7 +172,7 @@ class TreePathTestCase(TestCase):
                
                # Non-absolute result (binary search)
                self.assertQueryLimit(2, (second2, 'sub/path/tail'), 'root/second2/sub/path/tail', absolute_result=False)
-               self.assertQueryLimit(3, (second2, 'sub/'), 'root/second2/sub/', absolute_result=False)
+               self.assertQueryLimit(3, (second2, 'sub'), 'root/second2/sub/', absolute_result=False)
                self.assertQueryLimit(2, e, 'invalid/path/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/0', absolute_result=False)
                self.assertQueryLimit(1, (root, None), 'root', absolute_result=False)
                self.assertQueryLimit(2, (second2, None), 'root/second2', absolute_result=False)
@@ -203,8 +182,8 @@ class TreePathTestCase(TestCase):
                self.assertQueryLimit(1, (second2, None), 'second2', root=root, absolute_result=False)
                self.assertQueryLimit(2, (third, None), 'second/third', root=root, absolute_result=False)
                
-               # Preserve trailing slash
-               self.assertQueryLimit(2, (second2, 'sub/path/tail/'), 'root/second2/sub/path/tail/', absolute_result=False)
+               # Eliminate trailing slash
+               self.assertQueryLimit(2, (second2, 'sub/path/tail'), 'root/second2/sub/path/tail/', absolute_result=False)
                
                # Speed increase for leaf nodes - should this be tested?
                self.assertQueryLimit(1, (fifth, 'sub/path/tail/len/five'), 'root/second/third/fourth/fifth/sub/path/tail/len/five', absolute_result=False)
@@ -223,3 +202,17 @@ class TreePathTestCase(TestCase):
                self.assertQueryLimit(1, 'second/third', root, callable=third.get_path)
                self.assertQueryLimit(1, e, third, callable=second2.get_path)
                self.assertQueryLimit(1, '? - ?', root, ' - ', 'title', callable=third.get_path)
+
+
+class ContainerTestCase(TestCase):
+       def test_simple_containers(self):
+               t = Template(code="{% container one %}{% container two %}{% container three %}{% container two %}")
+               contentlet_specs, contentreference_specs = t.containers
+               self.assertEqual(len(contentreference_specs.keyOrder), 0)
+               self.assertEqual(contentlet_specs, ['one', 'two', 'three'])
+               
+               ct = ContentType.objects.get_for_model(Tag)
+               t = Template(code="{% container one references philo.tag as tag1 %}{% container two references philo.tag as tag2 %}{% container one references philo.tag as tag1 %}")
+               contentlet_specs, contentreference_specs = t.containers
+               self.assertEqual(len(contentlet_specs), 0)
+               self.assertEqual(contentreference_specs, SortedDict([('one', ct), ('two', ct)]))
diff --git a/philo/utils/lazycompat.py b/philo/utils/lazycompat.py
new file mode 100644 (file)
index 0000000..3876562
--- /dev/null
@@ -0,0 +1,97 @@
+try:
+       from django.utils.functional import empty, LazyObject, SimpleLazyObject
+except ImportError:
+       # Supply LazyObject and SimpleLazyObject for django < r16308
+       import operator
+       
+       
+       empty = object()
+       def new_method_proxy(func):
+               def inner(self, *args):
+                       if self._wrapped is empty:
+                               self._setup()
+                       return func(self._wrapped, *args)
+               return inner
+
+       class LazyObject(object):
+               """
+               A wrapper for another class that can be used to delay instantiation of the
+               wrapped class.
+
+               By subclassing, you have the opportunity to intercept and alter the
+               instantiation. If you don't need to do that, use SimpleLazyObject.
+               """
+               def __init__(self):
+                       self._wrapped = empty
+
+               __getattr__ = new_method_proxy(getattr)
+
+               def __setattr__(self, name, value):
+                       if name == "_wrapped":
+                               # Assign to __dict__ to avoid infinite __setattr__ loops.
+                               self.__dict__["_wrapped"] = value
+                       else:
+                               if self._wrapped is empty:
+                                       self._setup()
+                               setattr(self._wrapped, name, value)
+
+               def __delattr__(self, name):
+                       if name == "_wrapped":
+                               raise TypeError("can't delete _wrapped.")
+                       if self._wrapped is empty:
+                               self._setup()
+                       delattr(self._wrapped, name)
+
+               def _setup(self):
+                       """
+                       Must be implemented by subclasses to initialise the wrapped object.
+                       """
+                       raise NotImplementedError
+
+               # introspection support:
+               __members__ = property(lambda self: self.__dir__())
+               __dir__ = new_method_proxy(dir)
+
+
+       class SimpleLazyObject(LazyObject):
+               """
+               A lazy object initialised from any function.
+
+               Designed for compound objects of unknown type. For builtins or objects of
+               known type, use django.utils.functional.lazy.
+               """
+               def __init__(self, func):
+                       """
+                       Pass in a callable that returns the object to be wrapped.
+
+                       If copies are made of the resulting SimpleLazyObject, which can happen
+                       in various circumstances within Django, then you must ensure that the
+                       callable can be safely run more than once and will return the same
+                       value.
+                       """
+                       self.__dict__['_setupfunc'] = func
+                       super(SimpleLazyObject, self).__init__()
+
+               def _setup(self):
+                       self._wrapped = self._setupfunc()
+
+               __str__ = new_method_proxy(str)
+               __unicode__ = new_method_proxy(unicode)
+
+               def __deepcopy__(self, memo):
+                       if self._wrapped is empty:
+                               # We have to use SimpleLazyObject, not self.__class__, because the
+                               # latter is proxied.
+                               result = SimpleLazyObject(self._setupfunc)
+                               memo[id(self)] = result
+                               return result
+                       else:
+                               import copy
+                               return copy.deepcopy(self._wrapped, memo)
+
+               # Need to pretend to be the wrapped class, for the sake of objects that care
+               # about this (especially in equality tests)
+               __class__ = property(new_method_proxy(operator.attrgetter("__class__")))
+               __eq__ = new_method_proxy(operator.eq)
+               __hash__ = new_method_proxy(hash)
+               __nonzero__ = new_method_proxy(bool)
\ No newline at end of file
index d3054b9..2c2a952 100644 (file)
@@ -37,10 +37,10 @@ def node_view(request, path=None, **kwargs):
                raise Http404
        
        node = request.node
-       subpath = request.node.subpath
+       subpath = request.node._subpath
        
        # Explicitly disallow trailing slashes if we are otherwise at a node's url.
-       if request._cached_node_path != "/" and request._cached_node_path[-1] == "/" and subpath == "/":
+       if node._path != "/" and node._path[-1] == "/" and subpath == "/":
                return HttpResponseRedirect(node.get_absolute_url())
        
        if not node.handles_subpath(subpath):