From: Stephen Burrows Date: Wed, 1 Jun 2011 21:50:47 +0000 (-0400) Subject: Merge branch 'release' into develop X-Git-Tag: philo-0.9.1^2~8^2~9 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/ac54db264df3ba87306945a5494bd89e38266c38?hp=85eb51c030b89dba1c940a8508f1ae1ced1178d4 Merge branch 'release' into develop --- diff --git a/docs/index.rst b/docs/index.rst index 666e3ee..079185d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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+ `_ +* `Django 1.2+ `_ +* `django-mptt e734079+ `_ +* (Optional) `django-grappelli 2.0+ `_ +* (Optional) `south 0.7.2+ `_ +* (Optional) `recaptcha-django r6 `_ + +To contribute, please visit the `project website `_ or make a fork of the git repository on `GitHub `_ or `Gitorious `_. Feel free to join us on IRC at `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+ `_ -* `Django 1.2+ `_ -* `django-mptt e734079+ `_ -* (Optional) `django-grappelli 2.0+ `_ -* (Optional) `south 0.7.2+ `_ -* (Optional) `recaptcha-django r6 `_ - -To contribute, please visit the `project website `_ or make a fork of the git repository on `GitHub `_ or `Gitorious `_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo `_. diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index 2b253db..0000000 --- a/docs/intro.rst +++ /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 ` 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 diff --git a/docs/models/entities.rst b/docs/models/entities.rst index a394c6f..b39a253 100644 --- a/docs/models/entities.rst +++ b/docs/models/entities.rst @@ -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 index 0000000..eeb9ce8 --- /dev/null +++ b/docs/tutorials/getting-started.rst @@ -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:: + + + + Hello world! + + +

Hello world!

+

The time is {% now %}.

+ + + +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:: + + + + {% container page_title %} + + + {% container page_body as content %} + {% if content %} +

{{ content }}

+ {% endif %} +

The time is {% now %}.

+ + + +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 index 0000000..903dfea --- /dev/null +++ b/docs/tutorials/intro.rst @@ -0,0 +1,7 @@ +Tutorials +========= + +.. toctree:: + :maxdepth: 1 + + getting-started diff --git a/docs/what.rst b/docs/what.rst new file mode 100644 index 0000000..ac44619 --- /dev/null +++ b/docs/what.rst @@ -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. diff --git a/philo/admin/base.py b/philo/admin/base.py index 3a9458e..81916ab 100644 --- a/philo/admin/base.py +++ b/philo/admin/base.py @@ -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 diff --git a/philo/admin/forms/containers.py b/philo/admin/forms/containers.py index 246a954..987524f 100644 --- a/philo/admin/forms/containers.py +++ b/philo/admin/forms/containers.py @@ -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 = [] diff --git a/philo/admin/pages.py b/philo/admin/pages.py index fd8665b..3e8f0f1 100644 --- a/philo/admin/pages.py +++ b/philo/admin/pages.py @@ -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, { diff --git a/philo/admin/widgets.py b/philo/admin/widgets.py index 62a492b..c753850 100644 --- a/philo/admin/widgets.py +++ b/philo/admin/widgets.py @@ -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=()): diff --git a/philo/contrib/penfield/models.py b/philo/contrib/penfield/models.py index b8ca610..2a145fb 100644 --- a/philo/contrib/penfield/models.py +++ b/philo/contrib/penfield/models.py @@ -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 index 0000000..5d7d5e3 --- /dev/null +++ b/philo/contrib/shipherd/migrations/0003_auto__del_field_navigationitem_slug.py @@ -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'] diff --git a/philo/contrib/shipherd/models.py b/philo/contrib/shipherd/models.py index a520fbe..429faaa 100644 --- a/philo/contrib/shipherd/models.py +++ b/philo/contrib/shipherd/models.py @@ -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() diff --git a/philo/fixtures/test_fixtures.json b/philo/fixtures/test_fixtures.json index 4c55372..2bda0d1 100644 --- a/philo/fixtures/test_fixtures.json +++ b/philo/fixtures/test_fixtures.json @@ -91,8 +91,8 @@ "rght": 143, "view_object_id": 1, "view_content_type": [ - "penfield", - "blogview" + "philo", + "page" ], "parent": 1, "level": 1, @@ -1236,7 +1236,7 @@ "model": "philo.redirect", "fields": { "status_code": 302, - "target": "second" + "url_or_subpath": "second" } }, { @@ -1382,47 +1382,5 @@ "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 - } } ] diff --git a/philo/middleware.py b/philo/middleware.py index b90067a..037fdc8 100644 --- a/philo/middleware.py +++ b/philo/middleware.py @@ -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 index 0000000..7a79fec --- /dev/null +++ b/philo/migrations/0016_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py @@ -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'] diff --git a/philo/models/base.py b/philo/models/base.py index 02d8456..0218261 100644 --- a/philo/models/base.py +++ b/philo/models/base.py @@ -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 diff --git a/philo/models/nodes.py b/philo/models/nodes.py index 6eeb965..ab3bca5 100644 --- a/philo/models/nodes.py +++ b/philo/models/nodes.py @@ -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): diff --git a/philo/models/pages.py b/philo/models/pages.py index bdd9b42..ea3bb64 100644 --- a/philo/models/pages.py +++ b/philo/models/pages.py @@ -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' diff --git a/philo/static/admin/js/TagCreation.js b/philo/static/philo/js/TagCreation.js similarity index 91% rename from philo/static/admin/js/TagCreation.js rename to philo/static/philo/js/TagCreation.js index d08d41e..610a4f0 100644 --- a/philo/static/admin/js/TagCreation.js +++ b/philo/static/philo/js/TagCreation.js @@ -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)) diff --git a/philo/tests.py b/philo/tests.py index a0e0184..15ce752 100644 --- a/philo/tests.py +++ b/philo/tests.py @@ -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 %}

{{ hello|slice:'1:' }}

", "

root/second/

"), - ("{% 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 %}{{ goodbye|upper }}", "/ROOT/SECOND2/2010/10/20"), - ("{% 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 %}

{{ hello|slice:'1:' }}

", "

root/second

"), + ("{% 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 index 0000000..3876562 --- /dev/null +++ b/philo/utils/lazycompat.py @@ -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 diff --git a/philo/views.py b/philo/views.py index d3054b9..2c2a952 100644 --- a/philo/views.py +++ b/philo/views.py @@ -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):