Merge branch 'master' into navigation
[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
5 from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect
6 from django.core.exceptions import ViewDoesNotExist
7 from django.core.servers.basehttp import FileWrapper
8 from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
9 from django.template import add_to_builtins as register_templatetags
10 from inspect import getargspec
11 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
12 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
13 from philo.models.fields import JSONField
14 from philo.utils import ContentTypeSubclassLimiter
15 from philo.validators import RedirectValidator
16 from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
17 from philo.signals import view_about_to_render, view_finished_rendering
18 from mptt.templatetags.mptt_tags import cache_tree_children
19
20
21 _view_content_type_limiter = ContentTypeSubclassLimiter(None)
22 DEFAULT_NAVIGATION_DEPTH = 3
23
24
25 class Node(TreeEntity):
26         view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
27         view_object_id = models.PositiveIntegerField()
28         view = generic.GenericForeignKey('view_content_type', 'view_object_id')
29         
30         @property
31         def accepts_subpath(self):
32                 if self.view:
33                         return self.view.accepts_subpath
34                 return False
35         
36         def render_to_response(self, request, extra_context=None):
37                 return self.view.render_to_response(request, extra_context)
38         
39         def get_absolute_url(self):
40                 try:
41                         root = Site.objects.get_current().root_node
42                 except Site.DoesNotExist:
43                         root = None
44                 
45                 try:
46                         path = self.get_path(root=root)
47                         if path:
48                                 path += '/'
49                         root_url = reverse('philo-root')
50                         return '%s%s' % (root_url, path)
51                 except AncestorDoesNotExist, ViewDoesNotExist:
52                         return None
53         
54         def get_navigation(self, depth=DEFAULT_NAVIGATION_DEPTH):
55                 max_depth = depth + self.get_level()
56                 tree = cache_tree_children(self.get_descendants(include_self=True).filter(level__lte=max_depth))
57                 
58                 def get_nav(parent, nodes):
59                         node_overrides = dict([(override.child.pk, override) for override in NodeNavigationOverride.objects.filter(parent=parent, child__in=nodes).select_related('child')])
60                         
61                         navigation_list = []
62                         for node in nodes:
63                                 node._override = node_overrides.get(node.pk, None)
64                                 
65                                 if node._override:
66                                         if node._override.hide:
67                                                 continue
68                                         navigation = node._override.get_navigation(node, max_depth)
69                                 else:
70                                         navigation = node.view.get_navigation(node, max_depth)
71                                 
72                                 if not node.is_leaf_node() and node.get_level() < max_depth:
73                                         children = navigation.get('children', [])
74                                         children += get_nav(node, node.get_children())
75                                         navigation['children'] = children
76                                 
77                                 if 'children' in navigation:
78                                         navigation['children'].sort(cmp=lambda x,y: cmp(x['order'], y['order']))
79                                 
80                                 navigation_list.append(navigation)
81                         
82                         return navigation_list
83                 
84                 navigation = get_nav(self.parent, tree)
85                 root = navigation[0]
86                 navigation = [root] + root['children']
87                 del(root['children'])
88                 return navigation
89         
90         def save(self):
91                 super(Node, self).save()
92                 
93         
94         class Meta:
95                 app_label = 'philo'
96
97
98 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
99 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
100
101
102 class NodeNavigationOverride(Entity):
103         parent = models.ForeignKey(Node, related_name="child_navigation_overrides", blank=True, null=True)
104         child = models.ForeignKey(Node, related_name="navigation_overrides")
105         
106         title = models.CharField(max_length=100, blank=True)
107         url = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True)
108         order = models.PositiveSmallIntegerField(blank=True, null=True)
109         child_navigation = JSONField()
110         hide = models.BooleanField()
111         
112         def get_navigation(self, node, max_depth):
113                 default = node.view.get_navigation(node, max_depth)
114                 if self.url:
115                         default['url'] = self.url
116                 if self.title:
117                         default['title'] = self.title
118                 if self.order:
119                         default['order'] = self.order
120                 if isinstance(self.child_navigation, list) and node.get_level() < max_depth:
121                         child_navigation = self.child_navigation[:]
122                         
123                         for child in child_navigation:
124                                 child['url'] = default['url'] + child['url']
125                         
126                         if 'children' in default:
127                                 overridden = set([child['url'] for child in default['children']]) & set([child['url'] for child in self.child_navigation])
128                                 if overridden:
129                                         for child in default[:]:
130                                                 if child['url'] in overridden:
131                                                         default.remove(child)
132                                 default['children'] += self.child_navigation
133                         else:
134                                 default['children'] = self.child_navigation
135                 return default
136         
137         class Meta:
138                 ordering = ['order']
139                 unique_together = ('parent', 'child',)
140                 app_label = 'philo'
141
142
143 class View(Entity):
144         nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
145         
146         accepts_subpath = False
147         
148         def get_subpath(self, obj):
149                 if not self.accepts_subpath:
150                         raise ViewDoesNotProvideSubpaths
151                 
152                 view_name, args, kwargs = self.get_reverse_params(obj)
153                 try:
154                         return reverse(view_name, args=args, kwargs=kwargs, urlconf=self)
155                 except NoReverseMatch:
156                         raise ViewCanNotProvideSubpath
157         
158         def get_reverse_params(self, obj):
159                 """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
160                 raise NotImplementedError("View subclasses must implement get_reverse_params to support subpaths.")
161         
162         def attributes_with_node(self, node):
163                 return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
164         
165         def render_to_response(self, request, extra_context=None):
166                 if not hasattr(request, 'node'):
167                         raise MIDDLEWARE_NOT_CONFIGURED
168                 
169                 extra_context = extra_context or {}
170                 view_about_to_render.send(sender=self, request=request, extra_context=extra_context)
171                 response = self.actually_render_to_response(request, extra_context)
172                 view_finished_rendering.send(sender=self, response=response)
173                 return response
174         
175         def actually_render_to_response(self, request, extra_context=None):
176                 raise NotImplementedError('View subclasses must implement render_to_response.')
177         
178         def get_navigation(self, node, max_depth):
179                 """
180                 Subclasses should implement get_navigation to support auto-generated navigation.
181                 max_depth is the deepest `level` that should be generated; node is the node that
182                 is asking for the navigation. This method should return a dictionary of the form:
183                         {
184                                 'url': url,
185                                 'title': title,
186                                 'order': order, # None for no ordering.
187                                 'children': [ # Optional
188                                         <similar child navigation dictionaries>
189                                 ]
190                         }
191                 """
192                 raise NotImplementedError('View subclasses must implement get_navigation.')
193         
194         class Meta:
195                 abstract = True
196
197
198 _view_content_type_limiter.cls = View
199
200
201 class MultiView(View):
202         accepts_subpath = True
203         
204         @property
205         def urlpatterns(self, obj):
206                 raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
207         
208         def actually_render_to_response(self, request, extra_context=None):
209                 clear_url_caches()
210                 subpath = request.node.subpath
211                 if not subpath:
212                         subpath = ""
213                 subpath = "/" + subpath
214                 view, args, kwargs = resolve(subpath, urlconf=self)
215                 view_args = getargspec(view)
216                 if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None):
217                         if 'extra_context' in kwargs:
218                                 extra_context.update(kwargs['extra_context'])
219                         kwargs['extra_context'] = extra_context
220                 return view(request, *args, **kwargs)
221         
222         def reverse(self, view_name, args=None, kwargs=None, node=None):
223                 """Shortcut method to handle the common pattern of getting the absolute url for a multiview's
224                 subpaths."""
225                 subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
226                 if node is not None:
227                         return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/'))
228                 return subpath
229         
230         class Meta:
231                 abstract = True
232
233
234 class Redirect(View):
235         STATUS_CODES = (
236                 (302, 'Temporary'),
237                 (301, 'Permanent'),
238         )
239         target = models.CharField(max_length=200, validators=[RedirectValidator()])
240         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
241         
242         def actually_render_to_response(self, request, extra_context=None):
243                 response = HttpResponseRedirect(self.target)
244                 response.status_code = self.status_code
245                 return response
246         
247         class Meta:
248                 app_label = 'philo'
249
250
251 # Why does this exist?
252 class File(View):
253         """ For storing arbitrary files """
254         
255         mimetype = models.CharField(max_length=255)
256         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
257         
258         def actually_render_to_response(self, request, extra_context=None):
259                 wrapper = FileWrapper(self.file)
260                 response = HttpResponse(wrapper, content_type=self.mimetype)
261                 response['Content-Length'] = self.file.size
262                 return response
263         
264         class Meta:
265                 app_label = 'philo'
266         
267         def __unicode__(self):
268                 return self.file.name
269
270
271 register_templatetags('philo.templatetags.nodes')
272 register_value_model(Node)