Merge branch 'julian' of git://github.com/lapilofu/philo
[philo.git] / models / nodes.py
index ab8adf7..99be196 100644 (file)
@@ -3,12 +3,15 @@ from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.contrib.sites.models import Site, RequestSite
 from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404
+from django.core.exceptions import ValidationError
 from django.core.servers.basehttp import FileWrapper
 from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
 from django.template import add_to_builtins as register_templatetags
+from django.utils.encoding import smart_str
 from inspect import getargspec
 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
+from philo.models.fields import JSONField
 from philo.utils import ContentTypeSubclassLimiter
 from philo.validators import RedirectValidator
 from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
@@ -38,7 +41,7 @@ class Node(TreeEntity):
        def get_absolute_url(self, request=None, with_domain=False, secure=False):
                return self.construct_url(request=request, with_domain=with_domain, secure=secure)
        
-       def construct_url(self, subpath=None, request=None, with_domain=False, secure=False):
+       def construct_url(self, subpath="/", request=None, with_domain=False, secure=False):
                """
                This method will construct a URL based on the Node's location.
                If a request is passed in, that will be used as a backup in case
@@ -72,10 +75,10 @@ class Node(TreeEntity):
                else:
                        domain = ""
                
-               if not path:
+               if not path or subpath == "/":
                        subpath = subpath[1:]
                
-               return '%s%s%s%s' % (domain, root_url, path, subpath or "")
+               return '%s%s%s%s' % (domain, root_url, path, subpath)
        
        class Meta:
                app_label = 'philo'
@@ -112,8 +115,8 @@ class View(Entity):
                
                try:
                        subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
-               except NoReverseMatch:
-                       raise ViewCanNotProvideSubpath
+               except NoReverseMatch, e:
+                       raise ViewCanNotProvideSubpath(e.message)
                
                if node is not None:
                        return node.construct_url(subpath)
@@ -202,16 +205,69 @@ class MultiView(View):
                abstract = True
 
 
-class Redirect(View):
+class TargetURLModel(models.Model):
+       target_node = models.ForeignKey(Node, blank=True, null=True, related_name="%(app_label)s_%(class)s_related")
+       url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
+       reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.")
+       
+       def clean(self):
+               if not self.target_node and not self.url_or_subpath:
+                       raise ValidationError("Either a target node or a url must be defined.")
+               
+               if self.reversing_parameters and not (self.url_or_subpath or self.target_node):
+                       raise ValidationError("Reversing parameters require either a view name or a target node.")
+               
+               try:
+                       self.get_target_url()
+               except (NoReverseMatch, ViewCanNotProvideSubpath), e:
+                       raise ValidationError(e.message)
+               
+               super(TargetURLModel, self).clean()
+       
+       def get_reverse_params(self):
+               params = self.reversing_parameters
+               args = kwargs = None
+               if isinstance(params, list):
+                       args = params
+               elif isinstance(params, dict):
+                       # Convert unicode keys to strings for Python < 2.6.5. Compare
+                       # http://stackoverflow.com/questions/4598604/how-to-pass-unicode-keywords-to-kwargs
+                       kwargs = dict([(smart_str(k, 'ascii'), v) for k, v in params.items()])
+               return self.url_or_subpath, args, kwargs
+       
+       def get_target_url(self):
+               node = self.target_node
+               if node is not None and node.accepts_subpath and self.url_or_subpath:
+                       if self.reversing_parameters is not None:
+                               view_name, args, kwargs = self.get_reverse_params()
+                               subpath = node.view.reverse(view_name, args=args, kwargs=kwargs)
+                       else:
+                               subpath = self.url_or_subpath
+                               if subpath[0] != '/':
+                                       subpath = '/' + subpath
+                       return node.construct_url(subpath)
+               elif node is not None:
+                       return node.get_absolute_url()
+               else:
+                       if self.reversing_parameters is not None:
+                               view_name, args, kwargs = self.get_reverse_params()
+                               return reverse(view_name, args=args, kwargs=kwargs)
+                       return self.url_or_subpath
+       target_url = property(get_target_url)
+       
+       class Meta:
+               abstract = True
+
+
+class Redirect(TargetURLModel, View):
        STATUS_CODES = (
                (302, 'Temporary'),
                (301, 'Permanent'),
        )
-       target = models.CharField(max_length=200, validators=[RedirectValidator()])
        status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
        
        def actually_render_to_response(self, request, extra_context=None):
-               response = HttpResponseRedirect(self.target)
+               response = HttpResponseRedirect(self.target_url)
                response.status_code = self.status_code
                return response