Minor improvements to navigationoverrides, including admin support and working naviga...
[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                         
63                         for node in nodes:
64                                 node._override = node_overrides.get(node.pk, None)
65                                 
66                                 if node._override:
67                                         if node._override.hide:
68                                                 continue
69                                         navigation = node._override.get_navigation(node, max_depth)
70                                 else:
71                                         navigation = node.view.get_navigation(node, max_depth)
72                                 
73                                 if not node.is_leaf_node() and node.get_level() < max_depth:
74                                         children = navigation.get('children', [])
75                                         children += get_nav(node, node.get_children())
76                                         navigation['children'] = children
77                                 
78                                 if 'children' in navigation:
79                                         navigation['children'].sort(cmp=lambda x,y: cmp(x['order'], y['order']))
80                                 
81                                 navigation_list.append(navigation)
82                         
83                         return navigation_list
84                 
85                 return get_nav(self.parent, tree)
86         
87         def save(self):
88                 super(Node, self).save()
89                 
90         
91         class Meta:
92                 app_label = 'philo'
93
94
95 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
96 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
97
98
99 class NodeNavigationOverride(Entity):
100         parent = models.ForeignKey(Node, related_name="child_navigation_overrides", blank=True, null=True)
101         child = models.ForeignKey(Node, related_name="navigation_overrides")
102         
103         title = models.CharField(max_length=100, blank=True)
104         url = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True)
105         order = models.PositiveSmallIntegerField(blank=True, null=True)
106         child_navigation = JSONField()
107         hide = models.BooleanField()
108         
109         def get_navigation(self, node, depth, current_depth):
110                 if self.child_navigation:
111                         depth = current_depth
112                 default = node.view.get_navigation(depth, current_depth)
113                 if self.url:
114                         default['url'] = self.url
115                 if self.title:
116                         default['title'] = self.title
117                 if self.order:
118                         default['order'] = self.order
119                 if isinstance(self.child_navigation, list):
120                         if 'children' in default:
121                                 default['children'] += self.child_navigation
122                         else:
123                                 default['children'] = self.child_navigation
124                 return default
125         
126         class Meta:
127                 ordering = ['order']
128                 unique_together = ('parent', 'child',)
129                 app_label = 'philo'
130
131
132 class View(Entity):
133         nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
134         
135         accepts_subpath = False
136         
137         def get_subpath(self, obj):
138                 if not self.accepts_subpath:
139                         raise ViewDoesNotProvideSubpaths
140                 
141                 view_name, args, kwargs = self.get_reverse_params(obj)
142                 try:
143                         return reverse(view_name, args=args, kwargs=kwargs, urlconf=self)
144                 except NoReverseMatch:
145                         raise ViewCanNotProvideSubpath
146         
147         def get_reverse_params(self, obj):
148                 """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
149                 raise NotImplementedError("View subclasses must implement get_reverse_params to support subpaths.")
150         
151         def attributes_with_node(self, node):
152                 return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
153         
154         def render_to_response(self, request, extra_context=None):
155                 if not hasattr(request, 'node'):
156                         raise MIDDLEWARE_NOT_CONFIGURED
157                 
158                 extra_context = extra_context or {}
159                 view_about_to_render.send(sender=self, request=request, extra_context=extra_context)
160                 response = self.actually_render_to_response(request, extra_context)
161                 view_finished_rendering.send(sender=self, response=response)
162                 return response
163         
164         def actually_render_to_response(self, request, extra_context=None):
165                 raise NotImplementedError('View subclasses must implement render_to_response.')
166         
167         def get_navigation(self, node, max_depth):
168                 """
169                 Subclasses should implement get_navigation to support auto-generated navigation.
170                 max_depth is the deepest `level` that should be generated; node is the node that
171                 is asking for the navigation. This method should return a dictionary of the form:
172                         {
173                                 'url': url,
174                                 'title': title,
175                                 'order': order, # None for no ordering.
176                                 'children': [ # Optional
177                                         <similar child navigation dictionaries>
178                                 ]
179                         }
180                 """
181                 raise NotImplementedError('View subclasses must implement get_navigation.')
182         
183         class Meta:
184                 abstract = True
185
186
187 _view_content_type_limiter.cls = View
188
189
190 class MultiView(View):
191         accepts_subpath = True
192         
193         @property
194         def urlpatterns(self, obj):
195                 raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
196         
197         def actually_render_to_response(self, request, extra_context=None):
198                 clear_url_caches()
199                 subpath = request.node.subpath
200                 if not subpath:
201                         subpath = ""
202                 subpath = "/" + subpath
203                 view, args, kwargs = resolve(subpath, urlconf=self)
204                 view_args = getargspec(view)
205                 if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None):
206                         if 'extra_context' in kwargs:
207                                 extra_context.update(kwargs['extra_context'])
208                         kwargs['extra_context'] = extra_context
209                 return view(request, *args, **kwargs)
210         
211         class Meta:
212                 abstract = True
213
214
215 class Redirect(View):
216         STATUS_CODES = (
217                 (302, 'Temporary'),
218                 (301, 'Permanent'),
219         )
220         target = models.CharField(max_length=200, validators=[RedirectValidator()])
221         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
222         
223         def actually_render_to_response(self, request, extra_context=None):
224                 response = HttpResponseRedirect(self.target)
225                 response.status_code = self.status_code
226                 return response
227         
228         class Meta:
229                 app_label = 'philo'
230
231
232 # Why does this exist?
233 class File(View):
234         """ For storing arbitrary files """
235         
236         mimetype = models.CharField(max_length=255)
237         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
238         
239         def actually_render_to_response(self, request, extra_context=None):
240                 wrapper = FileWrapper(self.file)
241                 response = HttpResponse(wrapper, content_type=self.mimetype)
242                 response['Content-Length'] = self.file.size
243                 return response
244         
245         class Meta:
246                 app_label = 'philo'
247         
248         def __unicode__(self):
249                 return self.file.name
250
251
252 register_templatetags('philo.templatetags.nodes')
253 register_value_model(Node)