Minor correction to LazyNode's use of subpath to avoid NameErrors.
[philo.git] / models / nodes.py
1 from django.db import models
2 from django.contrib.contenttypes.models import ContentType
3 from django.contrib.contenttypes import generic
4 from django.contrib.sites.models import Site, RequestSite
5 from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404
6 from django.core.servers.basehttp import FileWrapper
7 from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
8 from django.template import add_to_builtins as register_templatetags
9 from inspect import getargspec
10 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
11 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
12 from philo.utils import ContentTypeSubclassLimiter
13 from philo.validators import RedirectValidator
14 from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
15 from philo.signals import view_about_to_render, view_finished_rendering
16
17
18 _view_content_type_limiter = ContentTypeSubclassLimiter(None)
19
20
21 class Node(TreeEntity):
22         view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
23         view_object_id = models.PositiveIntegerField()
24         view = generic.GenericForeignKey('view_content_type', 'view_object_id')
25         
26         @property
27         def accepts_subpath(self):
28                 if self.view:
29                         return self.view.accepts_subpath
30                 return False
31         
32         def handles_subpath(self, subpath):
33                 return self.view.handles_subpath(subpath)
34         
35         def render_to_response(self, request, extra_context=None):
36                 return self.view.render_to_response(request, extra_context)
37         
38         def get_absolute_url(self, request=None, with_domain=False, secure=False):
39                 return self.construct_url(request=request, with_domain=with_domain, secure=secure)
40         
41         def construct_url(self, subpath=None, request=None, with_domain=False, secure=False):
42                 """
43                 This method will construct a URL based on the Node's location.
44                 If a request is passed in, that will be used as a backup in case
45                 the Site lookup fails. The Site lookup takes precedence because
46                 it's what's used to find the root node. This will raise:
47                 - NoReverseMatch if philo-root is not reverseable
48                 - Site.DoesNotExist if a domain is requested but not buildable.
49                 - AncestorDoesNotExist if the root node of the site isn't an
50                   ancestor of this instance.
51                 """
52                 # Try reversing philo-root first, since we can't do anything if that fails.
53                 root_url = reverse('philo-root')
54                 
55                 try:
56                         current_site = Site.objects.get_current()
57                 except Site.DoesNotExist:
58                         if request is not None:
59                                 current_site = RequestSite(request)
60                         elif with_domain:
61                                 # If they want a domain and we can't figure one out,
62                                 # best to reraise the error to let them know.
63                                 raise
64                         else:
65                                 current_site = None
66                 
67                 root = getattr(current_site, 'root_node', None)
68                 path = self.get_path(root=root)
69                 
70                 if current_site and with_domain:
71                         domain = "http%s://%s" % (secure and "s" or "", current_site.domain)
72                 else:
73                         domain = ""
74                 
75                 if not path:
76                         subpath = subpath[1:]
77                 
78                 return '%s%s%s%s' % (domain, root_url, path, subpath or "")
79         
80         class Meta:
81                 app_label = 'philo'
82
83
84 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
85 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
86
87
88 class View(Entity):
89         nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
90         
91         accepts_subpath = False
92         
93         def handles_subpath(self, subpath):
94                 if not self.accepts_subpath and subpath != "/":
95                         return False
96                 return True
97         
98         def reverse(self, view_name=None, args=None, kwargs=None, node=None, obj=None):
99                 """Shortcut method to handle the common pattern of getting the
100                 absolute url for a view's subpaths."""
101                 if not self.accepts_subpath:
102                         raise ViewDoesNotProvideSubpaths
103                 
104                 if obj is not None:
105                         # Perhaps just override instead of combining?
106                         obj_view_name, obj_args, obj_kwargs = self.get_reverse_params(obj)
107                         if view_name is None:
108                                 view_name = obj_view_name
109                         args = list(obj_args) + list(args or [])
110                         obj_kwargs.update(kwargs or {})
111                         kwargs = obj_kwargs
112                 
113                 try:
114                         subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
115                 except NoReverseMatch:
116                         raise ViewCanNotProvideSubpath
117                 
118                 if node is not None:
119                         return node.construct_url(subpath)
120                 return subpath
121         
122         def get_reverse_params(self, obj):
123                 """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
124                 raise NotImplementedError("View subclasses must implement get_reverse_params to support subpaths.")
125         
126         def attributes_with_node(self, node):
127                 return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
128         
129         def render_to_response(self, request, extra_context=None):
130                 if not hasattr(request, 'node'):
131                         raise MIDDLEWARE_NOT_CONFIGURED
132                 
133                 extra_context = extra_context or {}
134                 view_about_to_render.send(sender=self, request=request, extra_context=extra_context)
135                 response = self.actually_render_to_response(request, extra_context)
136                 view_finished_rendering.send(sender=self, response=response)
137                 return response
138         
139         def actually_render_to_response(self, request, extra_context=None):
140                 raise NotImplementedError('View subclasses must implement actually_render_to_response.')
141         
142         class Meta:
143                 abstract = True
144
145
146 _view_content_type_limiter.cls = View
147
148
149 class MultiView(View):
150         accepts_subpath = True
151         
152         @property
153         def urlpatterns(self):
154                 raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
155         
156         def handles_subpath(self, subpath):
157                 if not super(MultiView, self).handles_subpath(subpath):
158                         return False
159                 try:
160                         resolve(subpath, urlconf=self)
161                 except Http404:
162                         return False
163                 return True
164         
165         def actually_render_to_response(self, request, extra_context=None):
166                 clear_url_caches()
167                 subpath = request.node.subpath
168                 view, args, kwargs = resolve(subpath, urlconf=self)
169                 view_args = getargspec(view)
170                 if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None):
171                         if 'extra_context' in kwargs:
172                                 extra_context.update(kwargs['extra_context'])
173                         kwargs['extra_context'] = extra_context
174                 return view(request, *args, **kwargs)
175         
176         def get_context(self):
177                 """Hook for providing instance-specific context - such as the value of a Field - to all views."""
178                 return {}
179         
180         def basic_view(self, field_name):
181                 """
182                 Given the name of a field on ``self``, accesses the value of
183                 that field and treats it as a ``View`` instance. Creates a
184                 basic context based on self.get_context() and any extra_context
185                 that was passed in, then calls the ``View`` instance's
186                 render_to_response() method. This method is meant to be called
187                 to return a view function appropriate for urlpatterns.
188                 """
189                 field = self._meta.get_field(field_name)
190                 view = getattr(self, field.name, None)
191                 
192                 def inner(request, extra_context=None, **kwargs):
193                         if not view:
194                                 raise Http404
195                         context = self.get_context()
196                         context.update(extra_context or {})
197                         return view.render_to_response(request, extra_context=context)
198                 
199                 return inner
200         
201         class Meta:
202                 abstract = True
203
204
205 class Redirect(View):
206         STATUS_CODES = (
207                 (302, 'Temporary'),
208                 (301, 'Permanent'),
209         )
210         target = models.CharField(max_length=200, validators=[RedirectValidator()])
211         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
212         
213         def actually_render_to_response(self, request, extra_context=None):
214                 response = HttpResponseRedirect(self.target)
215                 response.status_code = self.status_code
216                 return response
217         
218         class Meta:
219                 app_label = 'philo'
220
221
222 class File(View):
223         """ For storing arbitrary files """
224         
225         mimetype = models.CharField(max_length=255)
226         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
227         
228         def actually_render_to_response(self, request, extra_context=None):
229                 wrapper = FileWrapper(self.file)
230                 response = HttpResponse(wrapper, content_type=self.mimetype)
231                 response['Content-Length'] = self.file.size
232                 return response
233         
234         class Meta:
235                 app_label = 'philo'
236         
237         def __unicode__(self):
238                 return self.file.name
239
240
241 register_templatetags('philo.templatetags.nodes')
242 register_value_model(Node)