Merge branch 'master' of http://github.com/ithinksw/philo
[philo.git] / models / nodes.py
index 56cbb12..2bfb4fd 100644 (file)
@@ -3,14 +3,17 @@ 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.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.servers.basehttp import FileWrapper
-from django.core.urlresolvers import resolve, clear_url_caches
+from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
 from django.template import add_to_builtins as register_templatetags
 from inspect import getargspec
 from django.template import add_to_builtins as register_templatetags
 from inspect import getargspec
+from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
 from philo.utils import ContentTypeSubclassLimiter
 from philo.validators import RedirectValidator
 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
 from philo.utils import ContentTypeSubclassLimiter
 from philo.validators import RedirectValidator
-from philo.exceptions import ViewDoesNotProvideSubpaths
+from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
+from philo.signals import view_about_to_render, view_finished_rendering
 
 
 _view_content_type_limiter = ContentTypeSubclassLimiter(None)
 
 
 _view_content_type_limiter = ContentTypeSubclassLimiter(None)
@@ -27,8 +30,23 @@ class Node(TreeEntity):
                        return self.view.accepts_subpath
                return False
        
                        return self.view.accepts_subpath
                return False
        
-       def render_to_response(self, request, path=None, subpath=None, extra_context=None):
-               return self.view.render_to_response(self, request, path, subpath, extra_context)
+       def render_to_response(self, request, extra_context=None):
+               return self.view.render_to_response(request, extra_context)
+       
+       def get_absolute_url(self):
+               try:
+                       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:
                app_label = 'philo'
        
        class Meta:
                app_label = 'philo'
@@ -44,15 +62,33 @@ class View(Entity):
        accepts_subpath = False
        
        def get_subpath(self, obj):
        accepts_subpath = False
        
        def get_subpath(self, obj):
-               raise ViewDoesNotProvideSubpaths
+               if not self.accepts_subpath:
+                       raise ViewDoesNotProvideSubpaths
+               
+               view_name, args, kwargs = self.get_reverse_params(obj)
+               try:
+                       return reverse(view_name, args=args, kwargs=kwargs, urlconf=self)
+               except NoReverseMatch:
+                       raise ViewCanNotProvideSubpath
+       
+       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("View subclasses must implement get_reverse_params to support subpaths.")
        
        def attributes_with_node(self, node):
                return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
        
        
        def attributes_with_node(self, node):
                return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
        
-       def relationships_with_node(self, node):
-               return QuerySetMapper(self.relationship_set, passthrough=node.relationships)
+       def render_to_response(self, request, extra_context=None):
+               if not hasattr(request, 'node'):
+                       raise MIDDLEWARE_NOT_CONFIGURED
+               
+               extra_context = extra_context or {}
+               view_about_to_render.send(sender=self, request=request, extra_context=extra_context)
+               response = self.actually_render_to_response(request, extra_context)
+               view_finished_rendering.send(sender=self, response=response)
+               return response
        
        
-       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                raise NotImplementedError('View subclasses must implement render_to_response.')
        
        class Meta:
                raise NotImplementedError('View subclasses must implement render_to_response.')
        
        class Meta:
@@ -65,10 +101,13 @@ _view_content_type_limiter.cls = View
 class MultiView(View):
        accepts_subpath = True
        
 class MultiView(View):
        accepts_subpath = True
        
-       urlpatterns = []
+       @property
+       def urlpatterns(self, obj):
+               raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
        
        
-       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                clear_url_caches()
                clear_url_caches()
+               subpath = request.node.subpath
                if not subpath:
                        subpath = ""
                subpath = "/" + subpath
                if not subpath:
                        subpath = ""
                subpath = "/" + subpath
@@ -78,10 +117,38 @@ class MultiView(View):
                        if 'extra_context' in kwargs:
                                extra_context.update(kwargs['extra_context'])
                        kwargs['extra_context'] = extra_context
                        if 'extra_context' in kwargs:
                                extra_context.update(kwargs['extra_context'])
                        kwargs['extra_context'] = extra_context
-               if 'node' in view_args[0] or view_args[2] is not None:
-                       kwargs['node'] = node
                return view(request, *args, **kwargs)
        
                return view(request, *args, **kwargs)
        
+       def reverse(self, view_name, args=None, kwargs=None, node=None):
+               """Shortcut method to handle the common pattern of getting the absolute url for a multiview's
+               subpaths."""
+               subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
+               if node is not None:
+                       return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/'))
+               return subpath
+       
+       def get_context(self):
+               """Hook for providing instance-specific context - such as the value of a Field - to all views."""
+               return {}
+       
+       def basic_view(self, view_name):
+               """
+               Wraps a field name and returns a simple view function that will render that view
+               with a basic context. This assumes that the field name is a ForeignKey to a
+               model with a render_to_response method.
+               """
+               field = self._meta.get_field(view_name)
+               view = getattr(self, field.name, None)
+               
+               def inner(request, extra_context=None, **kwargs):
+                       if not view:
+                               raise Http404
+                       context = self.get_context()
+                       context.update(extra_context or {})
+                       return view.render_to_response(request, extra_context=context)
+               
+               return inner
+       
        class Meta:
                abstract = True
 
        class Meta:
                abstract = True
 
@@ -94,7 +161,7 @@ class Redirect(View):
        target = models.CharField(max_length=200, validators=[RedirectValidator()])
        status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
        
        target = models.CharField(max_length=200, validators=[RedirectValidator()])
        status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
        
-       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                response = HttpResponseRedirect(self.target)
                response.status_code = self.status_code
                return response
                response = HttpResponseRedirect(self.target)
                response.status_code = self.status_code
                return response
@@ -103,13 +170,14 @@ class Redirect(View):
                app_label = 'philo'
 
 
                app_label = 'philo'
 
 
+# Why does this exist?
 class File(View):
        """ For storing arbitrary files """
        
        mimetype = models.CharField(max_length=255)
        file = models.FileField(upload_to='philo/files/%Y/%m/%d')
        
 class File(View):
        """ For storing arbitrary files """
        
        mimetype = models.CharField(max_length=255)
        file = models.FileField(upload_to='philo/files/%Y/%m/%d')
        
-       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                wrapper = FileWrapper(self.file)
                response = HttpResponse(wrapper, content_type=self.mimetype)
                response['Content-Length'] = self.file.size
                wrapper = FileWrapper(self.file)
                response = HttpResponse(wrapper, content_type=self.mimetype)
                response['Content-Length'] = self.file.size