Merge branch 'node_url_refactor'
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 21 Oct 2010 19:57:01 +0000 (15:57 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 21 Oct 2010 19:57:01 +0000 (15:57 -0400)
contrib/penfield/models.py
fixtures/test_fixtures.json [new file with mode: 0644]
models/nodes.py
templatetags/nodes.py
tests.py [new file with mode: 0644]

index 35646b8..6da87a6 100644 (file)
@@ -3,9 +3,8 @@ from django.conf import settings
 from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
 from philo.exceptions import ViewCanNotProvideSubpath
 from django.conf.urls.defaults import url, patterns, include
-from django.core.urlresolvers import reverse
 from django.http import Http404
-from datetime import datetime
+from datetime import date, datetime
 from philo.utils import paginate
 from philo.contrib.penfield.validators import validate_pagination_count
 from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
@@ -75,7 +74,7 @@ class BlogView(MultiView, FeedMultiViewMixin):
        def per_page(self):
                return self.entries_per_page
        
-       def get_subpath(self, obj):
+       def get_reverse_params(self, obj):
                if isinstance(obj, BlogEntry):
                        if obj.blog == self.blog:
                                kwargs = {'slug': obj.slug}
@@ -85,25 +84,17 @@ class BlogView(MultiView, FeedMultiViewMixin):
                                                kwargs.update({'month': str(obj.date.month).zfill(2)})
                                                if self.entry_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
-                               return reverse(self.entry_view, urlconf=self, kwargs=kwargs)
+                               return self.entry_view, [], kwargs
                elif isinstance(obj, Tag):
                        if obj in self.blog.entry_tags:
-                               return reverse('entries_by_tag', urlconf=self, kwargs={'tag_slugs': obj.slug})
-               elif isinstance(obj, (str, unicode)):
-                       split_obj = obj.split(':')
-                       if len(split_obj) > 1:
-                               kwargs = {}
-                               try:
-                                       kwargs.update({'year': str(int(split_obj[1])).zfill(4)})
-                                       if len(split_obj) > 2:
-                                               kwargs.update({'month': str(int(split_obj[2])).zfill(2)})
-                                               if len(split_obj) > 3:
-                                                       kwargs.update({'day': str(int(split_obj[3])).zfill(2)})
-                                                       return reverse('entries_by_day', urlconf=self, kwargs=kwargs)
-                                               return reverse('entries_by_month', urlconf=self, kwargs=kwargs)
-                                       return reverse('entries_by_year', urlconf=self, kwargs=kwargs)
-                               except:
-                                       pass
+                               return 'entries_by_tag', [], {'tag_slugs': obj.slug}
+               elif isinstance(obj, (date, datetime)):
+                       kwargs = {
+                               'year': str(obj.year).zfill(4),
+                               'month': str(obj.month).zfill(2),
+                               'day': str(obj.day).zfill(2)
+                       }
+                       return 'entries_by_day', [], kwargs
                raise ViewCanNotProvideSubpath
        
        def get_context(self):
@@ -310,7 +301,7 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        def __unicode__(self):
                return self.newsletter.__unicode__()
        
-       def get_subpath(self, obj):
+       def get_reverse_params(self, obj):
                if isinstance(obj, NewsletterArticle):
                        if obj.newsletter == self.newsletter:
                                kwargs = {'slug': obj.slug}
@@ -320,10 +311,17 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                                                kwargs.update({'month': str(obj.date.month).zfill(2)})
                                                if self.article_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
-                               return reverse(self.article_view, urlconf=self, kwargs=kwargs)
+                               return self.article_view, [], kwargs
                elif isinstance(obj, NewsletterIssue):
                        if obj.newsletter == self.newsletter:
-                               return reverse('issue', urlconf=self, kwargs={'numbering': obj.numbering})
+                               return 'issue', [], {'numbering': obj.numbering}
+               elif isinstance(obj, (date, datetime)):
+                       kwargs = {
+                               'year': str(obj.year).zfill(4),
+                               'month': str(obj.month).zfill(2),
+                               'day': str(obj.day).zfill(2)
+                       }
+                       return 'articles_by_day', [], kwargs
                raise ViewCanNotProvideSubpath
        
        @property
diff --git a/fixtures/test_fixtures.json b/fixtures/test_fixtures.json
new file mode 100644 (file)
index 0000000..18f6962
--- /dev/null
@@ -0,0 +1,219 @@
+[
+    {
+        "pk": 1, 
+        "model": "philo.tag", 
+        "fields": {
+            "name": "Test tag", 
+            "slug": "test-tag"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "never", 
+            "parent": 3, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "blog", 
+            "parent": 3, 
+            "view_content_type": [
+                "penfield", 
+                "blogview"
+            ]
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "root", 
+            "parent": null, 
+            "view_content_type": [
+                "philo", 
+                "redirect"
+            ]
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.redirect", 
+        "fields": {
+            "status_code": 302, 
+            "target": "never"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Never is working!\r\n{% node_url %}", 
+            "name": "Never", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "never"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "An index page!\r\n{% node_url %}\r\n\r\n{% for entry in entries %}\r\n<h4><a href='{% node_url with entry %}'>{{ entry.title }}</a></h4>\r\n<div class='post content'>\r\n{{ entry.content }}\r\n</div>\r\n{% endfor %}", 
+            "name": "Index", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "index"
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Entry detail page.", 
+            "name": "Entry", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "entry"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Tag page!", 
+            "name": "Tag", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "tag"
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Entry archive page!", 
+            "name": "Entry Archives!", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "entry-archives"
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "tag archives...", 
+            "name": "Tag Archives", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "tag-archives"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 1, 
+            "title": "Never"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 2, 
+            "title": "Index"
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 3, 
+            "title": "Entry"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 4, 
+            "title": "Tag"
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 5, 
+            "title": "Entry Archive"
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "philo.page", 
+        "fields": {
+            "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 ae2e081..b16521b 100644 (file)
@@ -3,8 +3,9 @@ from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.contrib.sites.models import Site
 from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect
+from django.core.exceptions import ViewDoesNotExist
 from django.core.servers.basehttp import FileWrapper
-from django.core.urlresolvers import resolve, clear_url_caches
+from django.core.urlresolvers import resolve, clear_url_caches, reverse
 from django.template import add_to_builtins as register_templatetags
 from inspect import getargspec
 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
@@ -33,10 +34,18 @@ class Node(TreeEntity):
                return self.view.render_to_response(request, extra_context)
        
        def get_absolute_url(self):
-               root = Site.objects.get_current().root_node
                try:
-                       return '/%s' % self.get_path(root=root)
-               except AncestorDoesNotExist:
+                       root = Site.objects.get_current().root_node
+               except Site.DoesNotExist:
+                       root = None
+               
+               try:
+                       path = self.get_path(root=root)
+                       if path:
+                               path += '/'
+                       root_url = reverse('philo-root')
+                       return '%s%s' % (root_url, path)
+               except AncestorDoesNotExist, ViewDoesNotExist:
                        return None
        
        class Meta:
@@ -81,7 +90,13 @@ _view_content_type_limiter.cls = View
 class MultiView(View):
        accepts_subpath = True
        
-       urlpatterns = []
+       @property
+       def urlpatterns(self, obj):
+               raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
+       
+       def get_reverse_params(self, obj):
+               """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
+               raise NotImplementedError("MultiView subclasses must implement get_subpath.")
        
        def actually_render_to_response(self, request, extra_context=None):
                clear_url_caches()
index 0cb2289..8a98630 100644 (file)
 from django import template
 from django.conf import settings
 from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.template.defaulttags import kwarg_re
+from django.utils.encoding import smart_str
+from philo.exceptions import ViewCanNotProvideSubpath
 
 
 register = template.Library()
 
 
 class NodeURLNode(template.Node):
-       def __init__(self, node, with_obj, as_var):
-               if node is not None:
-                       self.node = template.Variable(node)
-               else:
-                       self.node = None
-               
-               if with_obj is not None:
-                       self.with_obj = template.Variable(with_obj)
-               else:
-                       self.with_obj = None
-               
+       def __init__(self, node, as_var, with_obj=None, view_name=None, args=None, kwargs=None):
                self.as_var = as_var
+               self.view_name = view_name
+               
+               # Because the following variables have already been compiled as filters if they exist, they don't need to be re-scanned as template variables.
+               self.node = node
+               self.with_obj = with_obj
+               self.args = args
+               self.kwargs = kwargs
        
        def render(self, context):
-               try:
-                       if self.node:
-                               node = self.node.resolve(context)
-                       else:
-                               node = context['node']
-                       current_site = Site.objects.get_current()
-                       if node.has_ancestor(current_site.root_node):
-                               url = '/' + node.get_path(root=current_site.root_node)
-                               if self.with_obj:
-                                       with_obj = self.with_obj.resolve(context)
-                                       subpath = node.view.get_subpath(with_obj)
-                                       if subpath[0] is '/':
-                                               subpath = subpath[1:]
-                                       url += subpath
-                       else:
+               if self.node:
+                       node = self.node.resolve(context)
+               else:
+                       node = context['node']
+               
+               if not node:
+                       return settings.TEMPLATE_STRING_IF_INVALID
+               
+               if self.with_obj is None and self.view_name is None:
+                       url = node.get_absolute_url()
+               else:
+                       if not node.view.accepts_subpath:
                                return settings.TEMPLATE_STRING_IF_INVALID
                        
-                       if self.as_var:
-                               context[self.as_var] = url
-                               return settings.TEMPLATE_STRING_IF_INVALID
+                       if self.with_obj is not None:
+                               try:
+                                       view_name, args, kwargs = node.view.get_reverse_params(self.with_obj.resolve(context))
+                               except ViewCanNotProvideSubpath:
+                                       return settings.TEMPLATE_STRING_IF_INVALID
+                       else: # self.view_name is not None
+                               view_name = self.view_name
+                               args = [arg.resolve(context) for arg in self.args]
+                               kwargs = dict([(smart_str(k, 'ascii'), v.resolve(context)) for k, v in self.kwargs.items()])
+                       
+                       url = ''
+                       try:
+                               subpath = reverse(view_name, urlconf=node.view, args=args, kwargs=kwargs)
+                       except NoReverseMatch:
+                               if self.as_var is None:
+                                       raise
                        else:
-                               return url
-               except:
-                       return settings.TEMPLATE_STRING_IF_INVALID
+                               if subpath[0] == '/':
+                                       subpath = subpath[1:]
+                               
+                               url = node.get_absolute_url() + subpath
+               
+               if self.as_var:
+                       context[self.as_var] = url
+                       return ''
+               else:
+                       return url
 
 
 @register.tag(name='node_url')
 def do_node_url(parser, token):
        """
-       {% node_url [<node>] [with <obj>] [as <var>] %}
+       {% node_url [for <node>] [as <var] %}
+       {% node_url with <obj> [for <node>] [as <var>] %}
+       {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
+       {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>]%}
        """
        params = token.split_contents()
        tag = params[0]
+       as_var = None
+       with_obj = None
+       node = None
+       params = params[1:]
        
-       if len(params) <= 6:
-               node = None
-               with_obj = None
-               as_var = None
-               remaining_tokens = params[1:]
-               while remaining_tokens:
-                       option_token = remaining_tokens.pop(0)
-                       if option_token == 'with':
-                               try:
-                                       with_obj = remaining_tokens.pop(0)
-                               except IndexError:
-                                       raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument specifying an object handled by the view on the node' % tag)
-                       elif option_token == 'as':
-                               try:
-                                       as_var = remaining_tokens.pop(0)
-                               except IndexError:
-                                       raise template.TemplateSyntaxError('"%s" template tag option "as" requires an argument specifying a variable name' % tag)
-                       else: # node
-                               node = option_token
-               return NodeURLNode(node=node, with_obj=with_obj, as_var=as_var)
-       else:
-               raise template.TemplateSyntaxError('"%s" template tag cannot accept more than five arguments' % tag)
\ No newline at end of file
+       if len(params) >= 2 and params[-2] == 'as':
+               as_var = params[-1]
+               params = params[:-2]
+       
+       if len(params) >= 2 and params[-2] == 'for':
+               node = parser.compile_filter(params[-1])
+               params = params[:-2]
+       
+       if len(params) >= 2 and params[-2] == 'with':
+               with_obj = parser.compile_filter(params[-1])
+               params = params[:-2]
+       
+       if with_obj is not None:
+               if params:
+                       raise template.TemplateSyntaxError('`%s` template tag accepts no arguments or keyword arguments if with <obj> is specified.' % tag)
+               return NodeURLNode(with_obj=with_obj, node=node, as_var=as_var)
+       
+       if params:
+               args = []
+               kwargs = {}
+               view_name = params.pop(0)
+               for param in params:
+                       match = kwarg_re.match(param)
+                       if not match:
+                               raise TemplateSyntaxError("Malformed arguments to `%s` tag" % tag)
+                       name, value = match.groups()
+                       if name:
+                               kwargs[name] = parser.compile_filter(value)
+                       else:
+                               args.append(parser.compile_filter(value))
+               return NodeURLNode(view_name=view_name, args=args, kwargs=kwargs, node=node, as_var=as_var)
+       
+       return NodeURLNode(node=node, as_var=as_var)
\ No newline at end of file
diff --git a/tests.py b/tests.py
new file mode 100644 (file)
index 0000000..d9f743f
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,74 @@
+from django.test import TestCase
+from django import template
+from django.conf import settings
+from philo.models import Node, Page, Template
+from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
+
+
+class NodeURLTestCase(TestCase):
+       """Tests the features of the node_url template tag."""
+       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)
+               
+               self.templates = [template.Template(string) for string in
+                       [
+                               "{% node_url %}", # 0
+                               "{% node_url for node2 %}", # 1
+                               "{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", # 2
+                               "{% node_url for nodes|first %}", # 3
+                               "{% node_url with entry %}", # 4
+                               "{% node_url with entry for node2 %}", # 5
+                               "{% node_url with tag for node2 %}", # 6
+                               "{% node_url with date for node2 %}", # 7
+                               "{% 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>", # 8
+                               "{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", # 9
+                               "{% node_url entries_by_year year=date|date:'Y' for node2 %}", # 10
+                       ]
+               ]
+               
+               nodes = Node.objects.all()
+               blog = Blog.objects.all()[0]
+               
+               self.context = template.Context({
+                       'node': nodes[0],
+                       'node2': nodes[1],
+                       'nodes': nodes,
+                       'entry': BlogEntry.objects.all()[0],
+                       'tag': blog.entry_tags.all()[0],
+                       'date': blog.entry_dates['day'][0]
+               })
+       
+       def test_nodeurl(self):
+               for i, template in enumerate(self.templates):
+                       t = template.render(self.context)
+                       
+                       if i == 0:
+                               self.assertEqual(t, "/root/never/")
+                       elif i == 1:
+                               self.assertEqual(t, "/root/blog/")
+                       elif i == 2:
+                               self.assertEqual(t, "<p>root/never/</p>")
+                       elif i == 3:
+                               self.assertEqual(t, "/root/never/")
+                       elif i == 4:
+                               self.assertEqual(t, settings.TEMPLATE_STRING_IF_INVALID)
+                       elif i == 5:
+                               self.assertEqual(t, "/root/blog/2010/10/20/first-entry")
+                       elif i == 6:
+                               self.assertEqual(t, "/root/blog/tags/test-tag/")
+                       elif i == 7:
+                               self.assertEqual(t, "/root/blog/2010/10/20")
+                       elif i == 8:
+                               self.assertEqual(t, "<em>/ROOT/BLOG/2010/10/20</em>")
+                       elif i == 9:
+                               self.assertEqual(t, "/root/blog/2010/10")
+                       elif i == 10:
+                               self.assertEqual(t, "/root/blog/2010/")
+                       else:
+                               print "Rendered as:\n%s\n\n" % t
\ No newline at end of file