Removed shipherd cache to see if it was really helping at all.
[philo.git] / philo / contrib / shipherd / models.py
1 #encoding: utf-8
2 from UserDict import DictMixin
3
4 from django.core.exceptions import ValidationError
5 from django.core.urlresolvers import NoReverseMatch
6 from django.core.validators import RegexValidator, MinValueValidator
7 from django.db import models
8 from django.forms.models import model_to_dict
9
10 from philo.models.base import TreeEntity, TreeEntityManager, Entity
11 from philo.models.nodes import Node, TargetURLModel
12
13
14 DEFAULT_NAVIGATION_DEPTH = 3
15
16
17 class NavigationMapper(object, DictMixin):
18         """
19         The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS`
20         
21         """
22         def __init__(self, node):
23                 self.node = node
24         
25         def __getitem__(self, key):
26                 return Navigation.objects.get_for_node(self.node, key)
27
28
29 def navigation(self):
30         if not hasattr(self, '_navigation'):
31                 self._navigation = NavigationMapper(self)
32         return self._navigation
33
34
35 Node.navigation = property(navigation)
36
37
38 class NavigationManager(models.Manager):
39         use_for_related = True
40         
41         def get_for_node(self, node, key):
42                 opts = Node._mptt_meta
43                 left = getattr(node, opts.left_attr)
44                 right = getattr(node, opts.right_attr)
45                 tree = getattr(node, opts.tree_id_attr)
46                 kwargs = {
47                         "node__%s__lte" % opts.left_attr: left,
48                         "node__%s__gte" % opts.right_attr: right,
49                         "node__%s" % opts.tree_id_attr: tree_id
50                 }
51                 navs = self.filter(key=key, **kwargs).order_by('-node__%s' % opts.level_attr)
52                 nav = navs[0]
53                 roots = nav.roots.all()
54                 item_opts = NavigationItem._mptt_meta
55                 by_pk = {}
56                 tree_ids = []
57                 
58                 for root in roots:
59                         by_pk[root.pk] = pk
60                         tree_ids.append(getattr(root, item_opts.tree_id_attr))
61                         root._cached_children = []
62                 
63                 kwargs = {
64                         '%s__in' % item_opts.tree_id_attr: tree_ids,
65                         '%s__lt' % item_opts.level_attr: nav.depth,
66                         '%s__gt' % item_opts.level_attr: 0
67                 }
68                 items = NavigationItem.objects.filter(**kwargs).order_by('level', 'order')
69                 for item in items:
70                         by_pk[item.pk] = item
71                         item._cached_children = []
72                         parent_pk = getattr(item, '%s_id' % item_opts.parent_attr)
73                         item.parent = by_pk[parent_pk]
74                         item.parent._cached_children.append(item)
75                 
76                 return roots
77
78
79 class Navigation(Entity):
80         """
81         :class:`Navigation` represents a group of :class:`NavigationItem`\ s that have an intrinsic relationship in terms of navigating a website. For example, a ``main`` navigation versus a ``side`` navigation, or a ``authenticated`` navigation versus an ``anonymous`` navigation.
82         
83         A :class:`Navigation`'s :class:`NavigationItem`\ s will be accessible from its related :class:`.Node` and that :class:`.Node`'s descendants through a :class:`NavigationMapper` instance at :attr:`Node.navigation`. Example::
84         
85                 >>> node.navigation_set.all()
86                 []
87                 >>> parent = node.parent
88                 >>> items = parent.navigation_set.get(key='main').roots.all()
89                 >>> parent.navigation["main"] == node.navigation["main"] == list(items)
90                 True
91         
92         """
93         #: A :class:`NavigationManager` instance.
94         objects = NavigationManager()
95         
96         #: The :class:`.Node` which the :class:`Navigation` is attached to. The :class:`Navigation` will also be available to all the :class:`.Node`'s descendants and will override any :class:`Navigation` with the same key on any of the :class:`.Node`'s ancestors.
97         node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
98         #: Each :class:`Navigation` has a ``key`` which consists of one or more word characters so that it can easily be accessed in a template as ``{{ node.navigation.this_key }}``.
99         key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
100         #: There is no limit to the depth of a tree of :class:`NavigationItem`\ s, but ``depth`` will limit how much of the tree will be displayed.
101         depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
102         
103         def __unicode__(self):
104                 return "%s[%s]" % (self.node, self.key)
105         
106         class Meta:
107                 unique_together = ('node', 'key')
108
109
110 class NavigationItem(TreeEntity, TargetURLModel):
111         #: A :class:`ForeignKey` to a :class:`Navigation` instance. If this is not null, then the :class:`NavigationItem` will be a root node of the :class:`Navigation` instance.
112         navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
113         #: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50.
114         text = models.CharField(max_length=50)
115         
116         #: The order in which the :class:`NavigationItem` will be displayed.
117         order = models.PositiveSmallIntegerField(default=0)
118         
119         def get_path(self, root=None, pathsep=u' › ', field='text'):
120                 return super(NavigationItem, self).get_path(root, pathsep, field)
121         path = property(get_path)
122         
123         def clean(self):
124                 super(NavigationItem, self).clean()
125                 if bool(self.parent) == bool(self.navigation):
126                         raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
127         
128         def is_active(self, request):
129                 """Returns ``True`` if the :class:`NavigationItem` is considered active for a given request and ``False`` otherwise."""
130                 if self.target_url == request.path:
131                         # Handle the `default` case where the target_url and requested path
132                         # are identical.
133                         return True
134                 
135                 if self.target_node is None and self.url_or_subpath == "http%s://%s%s" % (request.is_secure() and 's' or '', request.get_host(), request.path):
136                         # If there's no target_node, double-check whether it's a full-url
137                         # match.
138                         return True
139                 
140                 if self.target_node and not self.url_or_subpath:
141                         # If there is a target node and it's targeted simply, but the target URL is not
142                         # the same as the request path, check whether the target node is an ancestor
143                         # of the requested node. If so, this is active unless the target node
144                         # is the same as the ``host node`` for this navigation structure.
145                         try:
146                                 host_node = self.get_root().navigation.node
147                         except AttributeError:
148                                 pass
149                         else:
150                                 if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
151                                         return True
152                 
153                 return False
154         
155         def has_active_descendants(self, request):
156                 """Returns ``True`` if the :class:`NavigationItem` has active descendants and ``False`` otherwise."""
157                 for child in self.get_children():
158                         if child.is_active(request) or child.has_active_descendants(request):
159                                 return True
160                 return False