Updated links to the "project website" and made the READMEs consistent with the docs.
[philo.git] / philo / utils / __init__.py
1 from django.db import models
2 from django.contrib.contenttypes.models import ContentType
3 from django.core.paginator import Paginator, EmptyPage
4 from django.template import Context
5 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode
6
7
8 def fattr(*args, **kwargs):
9         """
10         Returns a wrapper which takes a function as its only argument and sets the key/value pairs passed in with kwargs as attributes on that function. This can be used as a decorator.
11         
12         Example::
13         
14                 >>> from philo.utils import fattr
15                 >>> @fattr(short_description="Hello World!")
16                 ... def x():
17                 ...     pass
18                 ... 
19                 >>> x.short_description
20                 'Hello World!'
21         
22         """
23         def wrapper(function):
24                 for key in kwargs:
25                         setattr(function, key, kwargs[key])
26                 return function
27         return wrapper
28
29
30 ### ContentTypeLimiters
31
32
33 class ContentTypeLimiter(object):
34         def q_object(self):
35                 return models.Q(pk__in=[])
36         
37         def add_to_query(self, query, *args, **kwargs):
38                 query.add_q(self.q_object(), *args, **kwargs)
39
40
41 class ContentTypeRegistryLimiter(ContentTypeLimiter):
42         """Can be used to limit the choices for a :class:`ForeignKey` or :class:`ManyToManyField` to the :class:`ContentType`\ s which have been registered with this limiter."""
43         def __init__(self):
44                 self.classes = []
45         
46         def register_class(self, cls):
47                 """Registers a model class with this limiter."""
48                 self.classes.append(cls)
49         
50         def unregister_class(self, cls):
51                 """Unregisters a model class from this limiter."""
52                 self.classes.remove(cls)
53         
54         def q_object(self):
55                 contenttype_pks = []
56                 for cls in self.classes:
57                         try:
58                                 if issubclass(cls, models.Model):
59                                         if not cls._meta.abstract:
60                                                 contenttype = ContentType.objects.get_for_model(cls)
61                                                 contenttype_pks.append(contenttype.pk)
62                         except:
63                                 pass
64                 return models.Q(pk__in=contenttype_pks)
65
66
67 class ContentTypeSubclassLimiter(ContentTypeLimiter):
68         """
69         Can be used to limit the choices for a :class:`ForeignKey` or :class:`ManyToManyField` to the :class:`ContentType`\ s for all non-abstract models which subclass the class passed in on instantiation.
70         
71         :param cls: The class whose non-abstract subclasses will be valid choices.
72         :param inclusive: Whether ``cls`` should also be considered a valid choice (if it is a non-abstract subclass of :class:`models.Model`)
73         
74         """
75         def __init__(self, cls, inclusive=False):
76                 self.cls = cls
77                 self.inclusive = inclusive
78         
79         def q_object(self):
80                 contenttype_pks = []
81                 def handle_subclasses(cls):
82                         for subclass in cls.__subclasses__():
83                                 try:
84                                         if issubclass(subclass, models.Model):
85                                                 if not subclass._meta.abstract:
86                                                         if not self.inclusive and subclass is self.cls:
87                                                                 continue
88                                                         contenttype = ContentType.objects.get_for_model(subclass)
89                                                         contenttype_pks.append(contenttype.pk)
90                                         handle_subclasses(subclass)
91                                 except:
92                                         pass
93                 handle_subclasses(self.cls)
94                 return models.Q(pk__in=contenttype_pks)
95
96
97 ### Pagination
98
99
100 def paginate(objects, per_page=None, page_number=1):
101         """
102         Given a list of objects, return a (``paginator``, ``page``, ``objects``) tuple.
103         
104         :param objects: The list of objects to be paginated.
105         :param per_page: The number of objects per page.
106         :param page_number: The number of the current page.
107         :returns tuple: (``paginator``, ``page``, ``objects``) where ``paginator`` is a :class:`django.core.paginator.Paginator` instance, ``page`` is the result of calling :meth:`Paginator.page` with ``page_number``, and objects is ``page.objects``. Any of the return values which can't be calculated will be returned as ``None``.
108         
109         """
110         try:
111                 per_page = int(per_page)
112         except (TypeError, ValueError):
113                 # Then either it wasn't set or it was set to an invalid value
114                 paginator = page = None
115         else:
116                 # There also shouldn't be pagination if the list is too short. Try count()
117                 # first - good chance it's a queryset, where count is more efficient.
118                 try:
119                         if objects.count() <= per_page:
120                                 paginator = page = None
121                 except AttributeError:
122                         if len(objects) <= per_page:
123                                 paginator = page = None
124         
125         try:
126                 return paginator, page, objects
127         except NameError:
128                 pass
129         
130         paginator = Paginator(objects, per_page)
131         try:
132                 page_number = int(page_number)
133         except:
134                 page_number = 1
135         
136         try:
137                 page = paginator.page(page_number)
138         except EmptyPage:
139                 page = None
140         else:
141                 objects = page.object_list
142         
143         return paginator, page, objects
144
145
146 ### Facilitating template analysis.
147
148
149 LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
150 BLANK_CONTEXT = Context()
151
152
153 def get_extended(self):
154         return self.get_parent(BLANK_CONTEXT)
155
156
157 def get_included(self):
158         return self.template
159
160
161 # We ignore the IncludeNode because it will never work in a blank context.
162 setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
163 setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))