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
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}
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):
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}
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
--- /dev/null
+[
+ {
+ "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
+ }
+ }
+]
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
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:
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()
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
--- /dev/null
+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