* Django 1.3+ <http://www.djangoproject.com/>
* django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>
* (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
- * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
* (Optional) south 0.7.2+ <http://south.aeracode.org/>
+ * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
+
+To contribute, please visit the project website <http://project.philocms.org/> and/or make a fork of the git repository on GitHub <http://github.com/ithinksw/philo> or Gitorious <http://gitorious
+.org/ithinksw/philo>. Feel free to join us on IRC at irc://irc.oftc.net/#philo.
-To contribute, please visit the project website <http://philocms.org/>. Feel free to join us on IRC at irc://irc.oftc.net/#philo.
====
Using philo
* (Optional) [south 0.7.2+ <http://south.aeracode.org/)](http://south.aeracode.org/)
* (Optional) [recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>](http://code.google.com/p/recaptcha-django/)
-To contribute, please visit the [project website](http://philocms.org/). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo>).
+To contribute, please visit the [project website](http://project.philocms.org/) and/or make a fork of the git repository on [GitHub](http://github.com/ithinksw/philo) or [Gitorious](http://gitorious
+.org/ithinksw/philo). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo).
Using philo
===========
shipherd
sobol
waldo
+ winer
.. automodule:: philo.contrib
.. autoclass:: philo.contrib.penfield.models.NewsletterView
:members:
-Abstract Syndication
-++++++++++++++++++++
-
-.. autoclass:: philo.contrib.penfield.models.FeedView
- :members:
-
-.. automodule:: philo.contrib.penfield.exceptions
- :members:
-
-.. automodule:: philo.contrib.penfield.middleware
- :members:
-
Template filters
++++++++++++++++
:members: Navigation, NavigationItem, NavigationMapper
:show-inheritance:
-Navigation caching
-------------------
-
.. autoclass:: NavigationManager
:members:
-.. autoclass:: NavigationItemManager
- :members:
-
-.. autoclass:: NavigationCacheQuerySet
- :members:
-
Template tags
+++++++++++++
--- /dev/null
+Winer
+=====
+
+.. automodule:: philo.contrib.winer
+
+.. automodule:: philo.contrib.winer.models
+
+ .. autoclass:: FeedView
+ :members:
+
+.. automodule:: philo.contrib.winer.exceptions
+ :members:
+
+.. automodule:: philo.contrib.winer.middleware
+ :members:
\ No newline at end of file
* (Optional) `south 0.7.2+ <http://south.aeracode.org/>`_
* (Optional) `recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>`_
-To contribute, please visit the `project website <http://philocms.org/>`_ or make a fork of the git repository on `GitHub <http://github.com/ithinksw/philo>`_ or `Gitorious <http://gitorious.org/ithinksw/philo>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
+To contribute, please visit the `project website <http://project.philocms.org/>`_ and/or make a fork of the git repository on `GitHub <http://github.com/ithinksw/philo>`_ or `Gitorious <http://gitorious.org/ithinksw/philo>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
Contents
++++++++
Next, add a philo :class:`.Page` - let's call it "Hello World Page" and use the template you just made.
-Now make a philo :class:`.Node`. Give it the slug ``hello-world``. Set the view content type to "Page" and use the page that you just made. If you navigate to ``/hello-world``, you will see the results of rendering the page!
+Now make a philo :class:`.Node`. Give it the slug ``hello-world``. Set the ``view_content_type`` to "Page" and the ``view_object_id`` to the id of the page that you just made - probably 1. If you navigate to ``/hello-world``, you will see the results of rendering the page!
Setting the root node
+++++++++++++++++++++
{% if content %}
<p>{{ content }}</p>
{% endif %}
- <p>The time is {% now %}.</p>
+ <p>The time is {% now "jS F Y H:i" %}.</p>
</body>
</html>
For this guide, we'll assume that you have the setup described in :doc:`getting-started`. We'll be adding a main :class:`.Navigation` to the root :class:`.Node` and making it display as part of the :class:`.Template`.
+Before getting started, make sure that you've added :mod:`philo.contrib.shipherd` to your :setting:`INSTALLED_APPS`. :mod:`~philo.contrib.shipherd` template tags also require the request context processor, so make sure to set :setting:`TEMPLATE_CONTEXT_PROCESSORS` appropriately::
+
+ TEMPLATE_CONTEXT_PROCESSORS = (
+ # Defaults
+ "django.contrib.auth.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media",
+ "django.core.context_processors.static",
+ "django.contrib.messages.context_processors.messages"
+ ...
+ "django.core.context_processors.request"
+ )
+
Creating the Navigation
+++++++++++++++++++++++
All you need to do now is show the navigation in the template! This is quite easy, using the :ttag:`~philo.contrib.shipherd.templatetags.shipherd.recursenavigation` templatetag. For now we'll keep it simple. Adjust the "Hello World Template" to look like this::
- <html>
+ <html>{% load shipherd %}
<head>
<title>{% container page_title %}</title>
</head>
<ul>
{% recursenavigation node "main" %}
<li{% if navloop.active %} class="active"{% endif %}>
- {{ item.text }}
+ <a href="{{ item.get_target_url }}">{{ item.text }}</a>
</li>
- {% endnavigation %}
+ {% endrecursenavigation %}
</ul>
{% container page_body as content %}
{% if content %}
-VERSION = (0, '1rc')
+VERSION = (0, 9)
# This is necessary because model forms store changes to self.instance in their clean method.
# Mutter mutter.
value = self.instance.value
- self._cached_value_ct = self.instance.value_content_type
+ self._cached_value_ct_id = self.instance.value_content_type_id
self._cached_value = value
# If there is a value, pull in its fields.
def save(self, *args, **kwargs):
# At this point, the cleaned_data has already been stored on self.instance.
- if self.instance.value_content_type != self._cached_value_ct:
+ if self.instance.value_content_type_id != self._cached_value_ct_id:
# The value content type has changed. Clear the old value, if there was one.
if self._cached_value:
self._cached_value.delete()
# Now create a new value instance so that on next instantiation, the form will
# know what fields to add.
- if self.instance.value_content_type is not None:
- self.instance.value = self.instance.value_content_type.model_class().objects.create()
+ if self.instance.value_content_type_id is not None:
+ self.instance.value = ContentType.objects.get_for_id(self.instance.value_content_type_id).model_class().objects.create()
elif self.instance.value is not None:
# The value content type is the same, but one of the value fields has changed.
"""
Following Python and Django’s “batteries included” philosophy, Philo includes a number of optional packages that simplify common website structures:
-* :mod:`~philo.contrib.penfield` — Basic philo syndication, and blog and newsletter management.
+* :mod:`~philo.contrib.penfield` — Basic blog and newsletter management.
* :mod:`~philo.contrib.shipherd` — Powerful site navigation.
* :mod:`~philo.contrib.sobol` — Custom web and database searches.
* :mod:`~philo.contrib.waldo` — Custom authentication systems.
+* :mod:`~philo.contrib.winer` — Abstract framework for Philo-based syndication.
"""
\ No newline at end of file
from django.utils.encoding import force_unicode
from philo.contrib.julian.feedgenerator import ICalendarFeed
-from philo.contrib.penfield.models import FeedView, FEEDS
+from philo.contrib.winer.models import FeedView
+from philo.contrib.winer.feeds import registry
from philo.exceptions import ViewCanNotProvideSubpath
from philo.models import Tag, Entity, Page
from philo.models.fields import TemplateField
__all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
-ICALENDAR = ICalendarFeed.mime_type
-FEEDS[ICALENDAR] = ICalendarFeed
+registry.register(ICalendarFeed, verbose_name="iCalendar")
try:
DEFAULT_SITE = Site.objects.get_current()
except:
# or per-calendar-view basis.
#url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
- if self.tag_archive_page:
+ if self.tag_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
)
- if self.owner_archive_page:
+ if self.owner_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
)
- if self.location_archive_page:
+ if self.location_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
)
def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
location = ct.model_class()._default_manager.get(pk=pk)
except ObjectDoesNotExist:
raise Http404
return u"%s for %s" % (self.__class__.__name__, self.calendar)
field = CalendarView._meta.get_field('feed_type')
-field._choices += ((ICALENDAR, 'iCalendar'),)
-field.default = ICALENDAR
\ No newline at end of file
+field.default = registry.get_slug(ICalendarFeed, field.default)
\ No newline at end of file
+++ /dev/null
-class HttpNotAcceptable(Exception):
- """This will be raised if an Http-Accept header will not accept the feed content types that are available."""
- pass
\ No newline at end of file
from django.conf import settings
from django.conf.urls.defaults import url, patterns, include
-from django.contrib.sites.models import Site, RequestSite
-from django.contrib.syndication.views import add_domain
from django.db import models
from django.http import Http404, HttpResponse
-from django.template import RequestContext, Template as DjangoTemplate
-from django.utils import feedgenerator, tzinfo
-from django.utils.datastructures import SortedDict
-from django.utils.encoding import smart_unicode, force_unicode
-from django.utils.html import escape
-from philo.contrib.penfield.exceptions import HttpNotAcceptable
-from philo.contrib.penfield.middleware import http_not_acceptable
+from philo.contrib.winer.models import FeedView
from philo.exceptions import ViewCanNotProvideSubpath
-from philo.models import Tag, Entity, MultiView, Page, register_value_model, Template
+from philo.models import Tag, Entity, Page, register_value_model
from philo.models.fields import TemplateField
from philo.utils import paginate
-try:
- import mimeparse
-except:
- mimeparse = None
-
-
-ATOM = feedgenerator.Atom1Feed.mime_type
-RSS = feedgenerator.Rss201rev2Feed.mime_type
-FEEDS = SortedDict([
- (ATOM, feedgenerator.Atom1Feed),
- (RSS, feedgenerator.Rss201rev2Feed),
-])
-FEED_CHOICES = (
- (ATOM, "Atom"),
- (RSS, "RSS"),
-)
-
-
-class FeedView(MultiView):
- """
- :class:`FeedView` handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.django.contrib.syndication.views.Feed>`_.
-
- """
- #: The type of feed which should be served by the :class:`FeedView`.
- feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
- #: The suffix which will be appended to a page URL for a feed of its items. Default: "feed"
- feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
- #: A :class:`BooleanField` - whether or not feeds are enabled.
- feeds_enabled = models.BooleanField(default=True)
- #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15.
- feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
-
- #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
- item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
- #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
- item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
-
- #: The name of the context variable to be populated with the items managed by the :class:`FeedView`.
- item_context_var = 'items'
- #: The attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`Blog`.)
- object_attr = 'object'
-
- #: A description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
- description = ""
-
- def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
- """
- Given the name to be used to reverse this view and the names of the attributes for the function that fetches the objects, returns patterns suitable for inclusion in urlpatterns.
-
- :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath.
- :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`.
- :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``.
- :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
- :returns: Patterns suitable for use in urlpatterns.
-
- Example::
-
- @property
- def urlpatterns(self):
- urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
- return urlpatterns
-
- """
- urlpatterns = patterns('')
- if self.feeds_enabled:
- feed_reverse_name = "%s_feed" % reverse_name
- feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
- feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
- urlpatterns += patterns('',
- url(feed_pattern, feed_view, name=feed_reverse_name),
- )
- urlpatterns += patterns('',
- url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
- )
- return urlpatterns
-
- def get_object(self, request, **kwargs):
- """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
- return getattr(self, self.object_attr)
-
- def feed_view(self, get_items_attr, reverse_name):
- """
- Returns a view function that renders a list of items as a feed.
-
- :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
- :param reverse_name: The name which can be used reverse this feed using the :class:`FeedView` as the urlconf.
-
- :returns: A view function that renders a list of items as a feed.
-
- """
- get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
-
- def inner(request, extra_context=None, *args, **kwargs):
- obj = self.get_object(request, *args, **kwargs)
- feed = self.get_feed(obj, request, reverse_name)
- items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs)
- self.populate_feed(feed, items, request)
-
- response = HttpResponse(mimetype=feed.mime_type)
- feed.write(response, 'utf-8')
- return response
-
- return inner
-
- def page_view(self, get_items_attr, page_attr):
- """
- :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
- :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``.
-
- :returns: A view function that renders a list of items as an :class:`HttpResponse`.
-
- """
- get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
- page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
-
- def inner(request, extra_context=None, *args, **kwargs):
- items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
- items, item_context = self.process_page_items(request, items)
-
- context = self.get_context()
- context.update(extra_context or {})
- context.update(item_context or {})
-
- return page.render_to_response(request, extra_context=context)
- return inner
-
- def process_page_items(self, request, items):
- """
- Hook for handling any extra processing of ``items`` based on an :class:`HttpRequest`, such as pagination or searching. This method is expected to return a list of items and a dictionary to be added to the page context.
-
- """
- item_context = {
- self.item_context_var: items
- }
- return items, item_context
-
- def get_feed_type(self, request):
- """
- Intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, then this method will raise :exc:`philo.contrib.penfield.exceptions.HttpNotAcceptable`.
-
- """
- feed_type = self.feed_type
- if feed_type not in FEEDS:
- feed_type = FEEDS.keys()[0]
- accept = request.META.get('HTTP_ACCEPT')
- if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
- # Wups! They aren't accepting the chosen format. Is there another format we can use?
- if mimeparse:
- feed_type = mimeparse.best_match(FEEDS.keys(), accept)
- else:
- for feed_type in FEEDS.keys():
- if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
- break
- else:
- feed_type = None
- if not feed_type:
- raise HttpNotAcceptable
- return FEEDS[feed_type]
-
- def get_feed(self, obj, request, reverse_name):
- """
- Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
-
- """
- try:
- current_site = Site.objects.get_current()
- except Site.DoesNotExist:
- current_site = RequestSite(request)
-
- feed_type = self.get_feed_type(request)
- node = request.node
- link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure())
-
- feed = feed_type(
- title = self.__get_dynamic_attr('title', obj),
- subtitle = self.__get_dynamic_attr('subtitle', obj),
- link = link,
- description = self.__get_dynamic_attr('description', obj),
- language = settings.LANGUAGE_CODE.decode(),
- feed_url = add_domain(
- current_site.domain,
- self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_secure()),
- request.is_secure()
- ),
- author_name = self.__get_dynamic_attr('author_name', obj),
- author_link = self.__get_dynamic_attr('author_link', obj),
- author_email = self.__get_dynamic_attr('author_email', obj),
- categories = self.__get_dynamic_attr('categories', obj),
- feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
- feed_guid = self.__get_dynamic_attr('feed_guid', obj),
- ttl = self.__get_dynamic_attr('ttl', obj),
- **self.feed_extra_kwargs(obj)
- )
- return feed
-
- def populate_feed(self, feed, items, request):
- """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
- if self.item_title_template:
- title_template = DjangoTemplate(self.item_title_template.code)
- else:
- title_template = None
- if self.item_description_template:
- description_template = DjangoTemplate(self.item_description_template.code)
- else:
- description_template = None
-
- node = request.node
- try:
- current_site = Site.objects.get_current()
- except Site.DoesNotExist:
- current_site = RequestSite(request)
-
- if self.feed_length is not None:
- items = items[:self.feed_length]
-
- for item in items:
- if title_template is not None:
- title = title_template.render(RequestContext(request, {'obj': item}))
- else:
- title = self.__get_dynamic_attr('item_title', item)
- if description_template is not None:
- description = description_template.render(RequestContext(request, {'obj': item}))
- else:
- description = self.__get_dynamic_attr('item_description', item)
-
- link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
-
- enc = None
- enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
- if enc_url:
- enc = feedgenerator.Enclosure(
- url = smart_unicode(add_domain(
- current_site.domain,
- enc_url,
- request.is_secure()
- )),
- length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
- mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
- )
- author_name = self.__get_dynamic_attr('item_author_name', item)
- if author_name is not None:
- author_email = self.__get_dynamic_attr('item_author_email', item)
- author_link = self.__get_dynamic_attr('item_author_link', item)
- else:
- author_email = author_link = None
-
- pubdate = self.__get_dynamic_attr('item_pubdate', item)
- if pubdate and not pubdate.tzinfo:
- ltz = tzinfo.LocalTimezone(pubdate)
- pubdate = pubdate.replace(tzinfo=ltz)
-
- feed.add_item(
- title = title,
- link = link,
- description = description,
- unique_id = self.__get_dynamic_attr('item_guid', item, link),
- enclosure = enc,
- pubdate = pubdate,
- author_name = author_name,
- author_email = author_email,
- author_link = author_link,
- categories = self.__get_dynamic_attr('item_categories', item),
- item_copyright = self.__get_dynamic_attr('item_copyright', item),
- **self.item_extra_kwargs(item)
- )
-
- def __get_dynamic_attr(self, attname, obj, default=None):
- try:
- attr = getattr(self, attname)
- except AttributeError:
- return default
- if callable(attr):
- # Check func_code.co_argcount rather than try/excepting the
- # function and catching the TypeError, because something inside
- # the function may raise the TypeError. This technique is more
- # accurate.
- if hasattr(attr, 'func_code'):
- argcount = attr.func_code.co_argcount
- else:
- argcount = attr.__call__.func_code.co_argcount
- if argcount == 2: # one argument is 'self'
- return attr(obj)
- else:
- return attr()
- return attr
-
- def feed_extra_kwargs(self, obj):
- """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
- return {}
-
- def item_extra_kwargs(self, item):
- """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
- return {}
-
- def item_title(self, item):
- return escape(force_unicode(item))
-
- def item_description(self, item):
- return force_unicode(item)
-
- class Meta:
- abstract=True
-
class Blog(Entity):
"""Represents a blog which can be posted to."""
class BlogView(FeedView):
"""
- A subclass of :class:`FeedView` which handles patterns and feeds for a :class:`Blog` and its related :class:`entries <BlogEntry>`.
+ A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Blog` and its related :class:`entries <BlogEntry>`.
"""
ENTRY_PERMALINK_STYLE_CHOICES = (
def get_reverse_params(self, obj):
if isinstance(obj, BlogEntry):
- if obj.blog == self.blog:
+ if obj.blog_id == self.blog_id:
kwargs = {'slug': obj.slug}
if self.entry_permalink_style in 'DMY':
kwargs.update({'year': str(obj.date.year).zfill(4)})
elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
if isinstance(obj, Tag):
obj = [obj]
- slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
+ slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset(self.blog)]
if slugs:
return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
elif isinstance(obj, (date, datetime)):
urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
- if self.tag_archive_page:
+ if self.tag_archive_page_id:
urlpatterns += patterns('',
url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
)
- if self.entry_archive_page:
+ if self.entry_archive_page_id:
if self.entry_permalink_style in 'DMY':
urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
if self.entry_permalink_style in 'DM':
def get_context(self):
return {'blog': self.blog}
- def get_entry_queryset(self):
+ def get_entry_queryset(self, obj):
"""Returns the default :class:`QuerySet` of :class:`BlogEntry` instances for the :class:`BlogView` - all entries that are considered posted in the past. This allows for scheduled posting of entries."""
- return self.blog.entries.filter(date__lte=datetime.now())
+ return obj.entries.filter(date__lte=datetime.now())
- def get_tag_queryset(self):
+ def get_tag_queryset(self, obj):
"""Returns the default :class:`QuerySet` of :class:`.Tag`\ s for the :class:`BlogView`'s :meth:`get_entries_by_tag` and :meth:`tag_archive_view`."""
- return self.blog.entry_tags
+ return obj.entry_tags
- def get_all_entries(self, request, extra_context=None):
- """Used to generate :meth:`~FeedView.feed_patterns` for all entries."""
- return self.get_entry_queryset(), extra_context
+ def get_all_entries(self, obj, request, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for all entries."""
+ return self.get_entry_queryset(obj), extra_context
- def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
- """Used to generate :meth:`~FeedView.feed_patterns` for entries with a specific year, month, and day."""
+ def get_entries_by_ymd(self, obj, request, year=None, month=None, day=None, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for entries with a specific year, month, and day."""
if not self.entry_archive_page:
raise Http404
- entries = self.get_entry_queryset()
+ entries = self.get_entry_queryset(obj)
if year:
entries = entries.filter(date__year=year)
if month:
context.update({'year': year, 'month': month, 'day': day})
return entries, context
- def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
- """Used to generate :meth:`~FeedView.feed_patterns` for entries with all of the given tags."""
+ def get_entries_by_tag(self, obj, request, tag_slugs, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for entries with all of the given tags."""
tag_slugs = tag_slugs.replace('+', '/').split('/')
- tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
+ tags = self.get_tag_queryset(obj).filter(slug__in=tag_slugs)
if not tags:
raise Http404
if slug and slug not in found_slugs:
raise Http404
- entries = self.get_entry_queryset()
+ entries = self.get_entry_queryset(obj)
for tag in tags:
entries = entries.filter(tags=tag)
def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
"""Renders :attr:`entry_page` with the entry specified by the given parameters."""
- entries = self.get_entry_queryset()
+ entries = self.get_entry_queryset(self.blog)
if year:
entries = entries.filter(date__year=year)
if month:
context = self.get_context()
context.update(extra_context or {})
context.update({
- 'tags': self.get_tag_queryset()
+ 'tags': self.get_tag_queryset(self.blog)
})
return self.tag_archive_page.render_to_response(request, extra_context=context)
- def feed_view(self, get_items_attr, reverse_name):
- """Overrides :meth:`FeedView.feed_view` to add :class:`.Tag`\ s to the feed as categories."""
+ def feed_view(self, get_items_attr, reverse_name, feed_type=None):
+ """Overrides :meth:`.FeedView.feed_view` to add :class:`.Tag`\ s to the feed as categories."""
get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
def inner(request, extra_context=None, *args, **kwargs):
obj = self.get_object(request, *args, **kwargs)
- feed = self.get_feed(obj, request, reverse_name)
+ feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs)
items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
self.populate_feed(feed, items, request)
return inner
def process_page_items(self, request, items):
- """Overrides :meth:`FeedView.process_page_items` to add pagination."""
+ """Overrides :meth:`.FeedView.process_page_items` to add pagination."""
if self.entries_per_page:
page_num = request.GET.get('page', 1)
paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
class NewsletterView(FeedView):
- """A subclass of :class:`FeedView` which handles patterns and feeds for a :class:`Newsletter` and its related :class:`articles <NewsletterArticle>`."""
+ """A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Newsletter` and its related :class:`articles <NewsletterArticle>`."""
ARTICLE_PERMALINK_STYLE_CHOICES = (
('D', 'Year, month, and day'),
('M', 'Year and month'),
def get_reverse_params(self, obj):
if isinstance(obj, NewsletterArticle):
- if obj.newsletter == self.newsletter:
+ if obj.newsletter_id == self.newsletter_id:
kwargs = {'slug': obj.slug}
if self.article_permalink_style in 'DMY':
kwargs.update({'year': str(obj.date.year).zfill(4)})
kwargs.update({'day': str(obj.date.day).zfill(2)})
return self.article_view, [], kwargs
elif isinstance(obj, NewsletterIssue):
- if obj.newsletter == self.newsletter:
+ if obj.newsletter_id == self.newsletter_id:
return 'issue', [], {'numbering': obj.numbering}
elif isinstance(obj, (date, datetime)):
kwargs = {
urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
)
- if self.issue_archive_page:
+ if self.issue_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
)
- if self.article_archive_page:
- urlpatterns += patterns('',
- url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
- )
+ if self.article_archive_page_id:
+ urlpatterns += self.feed_patterns(r'^%s' % self.article_permalink_base, 'get_all_articles', 'article_archive_page', 'articles')
if self.article_permalink_style in 'DMY':
urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
if self.article_permalink_style in 'DM':
def get_context(self):
return {'newsletter': self.newsletter}
- def get_article_queryset(self):
+ def get_article_queryset(self, obj):
"""Returns the default :class:`QuerySet` of :class:`NewsletterArticle` instances for the :class:`NewsletterView` - all articles that are considered posted in the past. This allows for scheduled posting of articles."""
- return self.newsletter.articles.filter(date__lte=datetime.now())
+ return obj.articles.filter(date__lte=datetime.now())
- def get_issue_queryset(self):
+ def get_issue_queryset(self, obj):
"""Returns the default :class:`QuerySet` of :class:`NewsletterIssue` instances for the :class:`NewsletterView`."""
- return self.newsletter.issues.all()
+ return obj.issues.all()
- def get_all_articles(self, request, extra_context=None):
- """Used to generate :meth:`FeedView.feed_patterns` for all entries."""
- return self.get_article_queryset(), extra_context
+ def get_all_articles(self, obj, request, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for all entries."""
+ return self.get_article_queryset(obj), extra_context
- def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
- """Used to generate :meth:`FeedView.feed_patterns` for a specific year, month, and day."""
- articles = self.get_article_queryset().filter(date__year=year)
+ def get_articles_by_ymd(self, obj, request, year, month=None, day=None, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for a specific year, month, and day."""
+ articles = self.get_article_queryset(obj).filter(date__year=year)
if month:
articles = articles.filter(date__month=month)
if day:
articles = articles.filter(date__day=day)
return articles, extra_context
- def get_articles_by_issue(self, request, numbering, extra_context=None):
- """Used to generate :meth:`FeedView.feed_patterns` for articles from a certain issue."""
+ def get_articles_by_issue(self, obj, request, numbering, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for articles from a certain issue."""
try:
- issue = self.get_issue_queryset().get(numbering=numbering)
+ issue = self.get_issue_queryset(obj).get(numbering=numbering)
except NewsletterIssue.DoesNotExist:
raise Http404
context = extra_context or {}
context.update({'issue': issue})
- return self.get_article_queryset().filter(issues=issue), context
+ return self.get_article_queryset(obj).filter(issues=issue), context
def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
"""Renders :attr:`article_page` with the article specified by the given parameters."""
- articles = self.get_article_queryset()
+ articles = self.get_article_queryset(self.newsletter)
if year:
articles = articles.filter(date__year=year)
if month:
context = self.get_context()
context.update(extra_context or {})
context.update({
- 'issues': self.get_issue_queryset()
+ 'issues': self.get_issue_queryset(self.newsletter)
})
return self.issue_archive_page.render_to_response(request, extra_context=context)
class NavigationItemInline(admin.StackedInline):
raw_id_fields = NAVIGATION_RAW_ID_FIELDS
model = NavigationItem
- extra = 1
+ extra = 0
sortable_field_name = 'order'
+ ordering = ('order',)
related_lookup_fields = {'fk': raw_id_fields}
class NodeNavigationInline(admin.TabularInline):
model = Navigation
- extra = 1
+ extra = 0
NodeAdmin.inlines = [NodeNavigationInline, NodeNavigationItemInline] + NodeAdmin.inlines
#encoding: utf-8
from UserDict import DictMixin
+from hashlib import sha1
+from django.contrib.sites.models import Site
+from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.urlresolvers import NoReverseMatch
from django.core.validators import RegexValidator, MinValueValidator
class NavigationMapper(object, DictMixin):
"""
- 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. The fetching goes through the :class:`NavigationManager` and can thus take advantage of the navigation cache. 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`
+ 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`
"""
def __init__(self, node):
self.node = node
+ self._cache = {}
def __getitem__(self, key):
- return Navigation.objects.get_cache_for(self.node)[key]['root_items']
-
- def keys(self):
- return Navigation.objects.get_cache_for(self.node).keys()
+ if key not in self._cache:
+ try:
+ self._cache[key] = Navigation.objects.get_for_node(self.node, key)
+ except Navigation.DoesNotExist:
+ self._cache[key] = None
+ return self._cache[key]
def navigation(self):
Node.navigation = property(navigation)
-class NavigationCacheQuerySet(models.query.QuerySet):
- """
- This subclass will trigger general cache clearing for Navigation.objects when a mass
- update or deletion is performed. As there is no convenient way to iterate over the
- changed or deleted instances, there's no way to be more precise about what gets cleared.
-
- """
- def update(self, *args, **kwargs):
- super(NavigationCacheQuerySet, self).update(*args, **kwargs)
- Navigation.objects.clear_cache()
-
- def delete(self, *args, **kwargs):
- super(NavigationCacheQuerySet, self).delete(*args, **kwargs)
- Navigation.objects.clear_cache()
-
-
class NavigationManager(models.Manager):
- """
- Since navigation on a site will be hit frequently, is relatively costly to compute, and is changed relatively infrequently, the NavigationManager maintains a cache which maps nodes to navigations.
-
- """
use_for_related = True
- _cache = {}
- def get_query_set(self):
- """
- Returns a :class:`NavigationCacheQuerySet` instance.
-
- """
- return NavigationCacheQuerySet(self.model, using=self._db)
-
- def get_cache_for(self, node, update_targets=True):
- """Returns the navigation cache for a given :class:`.Node`. If update_targets is ``True``, then :meth:`update_targets_for` will be run with the :class:`.Node`."""
- created = False
- if not self.has_cache_for(node):
- self.create_cache_for(node)
- created = True
-
- if update_targets and not created:
- self.update_targets_for(node)
-
- return self.__class__._cache[self.db][node]
-
- def has_cache_for(self, node):
- """Returns ``True`` if a cache exists for the :class:`.Node` and ``False`` otherwise."""
- return self.db in self.__class__._cache and node in self.__class__._cache[self.db]
-
- def create_cache_for(self, node):
- """This method loops through the :class:`.Node`\ s ancestors and caches all unique navigation keys."""
- ancestors = node.get_ancestors(ascending=True, include_self=True)
-
- nodes_to_cache = []
-
- for node in ancestors:
- if self.has_cache_for(node):
- cache = self.get_cache_for(node).copy()
- break
- else:
- nodes_to_cache.insert(0, node)
- else:
- cache = {}
-
- for node in nodes_to_cache:
- cache = cache.copy()
- cache.update(self._build_cache_for(node))
- self.__class__._cache.setdefault(self.db, {})[node] = cache
-
- def _build_cache_for(self, node):
- cache = {}
- tree_id_attr = NavigationItem._mptt_meta.tree_id_attr
- level_attr = NavigationItem._mptt_meta.level_attr
-
- for navigation in node.navigation_set.all():
- tree_ids = navigation.roots.values_list(tree_id_attr)
- items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft'))
+ def get_for_node(self, node, key):
+ cache_key = self._get_cache_key(node, key)
+ cached = cache.get(cache_key)
+
+ if cached is None:
+ opts = Node._mptt_meta
+ left = getattr(node, opts.left_attr)
+ right = getattr(node, opts.right_attr)
+ tree_id = getattr(node, opts.tree_id_attr)
+ kwargs = {
+ "node__%s__lte" % opts.left_attr: left,
+ "node__%s__gte" % opts.right_attr: right,
+ "node__%s" % opts.tree_id_attr: tree_id
+ }
+ navs = self.filter(key=key, **kwargs).select_related('node').order_by('-node__%s' % opts.level_attr)
+ nav = navs[0]
+ roots = nav.roots.all().select_related('target_node').order_by('order')
+ item_opts = NavigationItem._mptt_meta
+ by_pk = {}
+ tree_ids = []
- root_items = []
+ site_root_node = Site.objects.get_current().root_node
- for item in items:
- item._is_cached = True
-
- if not hasattr(item, '_cached_children'):
- item._cached_children = []
-
- if item.parent:
- # alternatively, if I don't want to force it to a list, I could keep track of
- # instances where the parent hasn't yet been met and do this step later for them.
- # delayed action.
- item.parent = items[items.index(item.parent)]
- if not hasattr(item.parent, '_cached_children'):
- item.parent._cached_children = []
- item.parent._cached_children.append(item)
- else:
- root_items.append(item)
+ for root in roots:
+ by_pk[root.pk] = root
+ tree_ids.append(getattr(root, item_opts.tree_id_attr))
+ root._cached_children = []
+ if root.target_node:
+ root.target_node.get_path(root=site_root_node)
+ root.navigation = nav
- cache[navigation.key] = {
- 'navigation': navigation,
- 'root_items': root_items,
- 'items': items
+ kwargs = {
+ '%s__in' % item_opts.tree_id_attr: tree_ids,
+ '%s__lt' % item_opts.level_attr: nav.depth,
+ '%s__gt' % item_opts.level_attr: 0
}
+ items = NavigationItem.objects.filter(**kwargs).select_related('target_node').order_by('level', 'order')
+ for item in items:
+ by_pk[item.pk] = item
+ item._cached_children = []
+ parent_pk = getattr(item, '%s_id' % item_opts.parent_attr)
+ item.parent = by_pk[parent_pk]
+ item.parent._cached_children.append(item)
+ if item.target_node:
+ item.target_node.get_path(root=site_root_node)
+
+ cached = roots
+ cache.set(cache_key, cached)
- return cache
-
- def clear_cache_for(self, node):
- """Clear the cache for the :class:`.Node` and all its descendants. The navigation for this node has probably changed, and it isn't worth it to figure out which descendants were actually affected by this."""
- if not self.has_cache_for(node):
- # Already cleared.
- return
-
- descendants = node.get_descendants(include_self=True)
- cache = self.__class__._cache[self.db]
- for node in descendants:
- cache.pop(node, None)
+ return cached
- def update_targets_for(self, node):
- """Manually updates the target nodes for the :class:`.Node`'s cache in case something's changed there. This is a less complex operation than rebuilding the :class:`.Node`'s cache."""
- caches = self.__class__._cache[self.db][node].values()
-
- target_pks = set()
-
- for cache in caches:
- target_pks |= set([item.target_node_id for item in cache['items']])
-
- # A distinct query is not strictly necessary. TODO: benchmark the efficiency
- # with/without distinct.
- targets = list(Node.objects.filter(pk__in=target_pks).distinct())
+ def _get_cache_key(self, node, key):
+ opts = Node._mptt_meta
+ left = getattr(node, opts.left_attr)
+ right = getattr(node, opts.right_attr)
+ tree_id = getattr(node, opts.tree_id_attr)
+ parent_id = getattr(node, "%s_id" % opts.parent_attr)
- for cache in caches:
- for item in cache['items']:
- if item.target_node_id:
- item.target_node = targets[targets.index(item.target_node)]
-
- def clear_cache(self):
- """Clears the manager's entire navigation cache."""
- self.__class__._cache.pop(self.db, None)
+ return sha1(unicode(left) + unicode(right) + unicode(tree_id) + unicode(parent_id) + unicode(node.pk) + unicode(key)).hexdigest()
class Navigation(Entity):
#: 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.
depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
- def __init__(self, *args, **kwargs):
- super(Navigation, self).__init__(*args, **kwargs)
- self._initial_data = model_to_dict(self)
-
def __unicode__(self):
return "%s[%s]" % (self.node, self.key)
- def _has_changed(self):
- return self._initial_data != model_to_dict(self)
-
- def save(self, *args, **kwargs):
- super(Navigation, self).save(*args, **kwargs)
-
- if self._has_changed():
- Navigation.objects.clear_cache_for(self.node)
- self._initial_data = model_to_dict(self)
-
- def delete(self, *args, **kwargs):
- super(Navigation, self).delete(*args, **kwargs)
- Navigation.objects.clear_cache_for(self.node)
-
class Meta:
unique_together = ('node', 'key')
-class NavigationItemManager(TreeEntityManager):
- use_for_related = True
-
- def get_query_set(self):
- """Returns a :class:`NavigationCacheQuerySet` instance."""
- return NavigationCacheQuerySet(self.model, using=self._db)
-
-
class NavigationItem(TreeEntity, TargetURLModel):
- #: A :class:`NavigationItemManager` instance
- objects = NavigationItemManager()
-
#: 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.
navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
#: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50.
#: The order in which the :class:`NavigationItem` will be displayed.
order = models.PositiveSmallIntegerField(default=0)
- def __init__(self, *args, **kwargs):
- super(NavigationItem, self).__init__(*args, **kwargs)
- self._initial_data = model_to_dict(self)
- self._is_cached = False
-
def get_path(self, root=None, pathsep=u' › ', field='text'):
return super(NavigationItem, self).get_path(root, pathsep, field)
path = property(get_path)
# the same as the request path, check whether the target node is an ancestor
# of the requested node. If so, this is active unless the target node
# is the same as the ``host node`` for this navigation structure.
- try:
- host_node = self.get_root().navigation.node
- except AttributeError:
- pass
- else:
- if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
- return True
+ root = self
+
+ # The common case will be cached items, whose parents are cached with them.
+ while root.parent is not None:
+ root = root.parent
+
+ host_node_id = root.navigation.node_id
+ if self.target_node.pk != host_node_id and self.target_node.is_ancestor_of(request.node):
+ return True
return False
for child in self.get_children():
if child.is_active(request) or child.has_active_descendants(request):
return True
- return False
-
- def _has_changed(self):
- if model_to_dict(self) == self._initial_data:
- return False
- return True
-
- def _clear_cache(self):
- try:
- root = self.get_root()
- if self.get_level() < root.navigation.depth:
- Navigation.objects.clear_cache_for(self.get_root().navigation.node)
- except AttributeError:
- pass
-
- def save(self, *args, **kwargs):
- super(NavigationItem, self).save(*args, **kwargs)
-
- if self._has_changed():
- self._clear_cache()
-
- def delete(self, *args, **kwargs):
- super(NavigationItem, self).delete(*args, **kwargs)
- self._clear_cache()
\ No newline at end of file
+ return False
\ No newline at end of file
<ul>
{% recursenavigation node "main" %}
<li{% if navloop.active %} class='active'{% endif %}>
- {{ item.text }}
+ <a href="{{ item.get_target_url }}">{{ item.text }}</a>
{% if item.get_children %}
<ul>
{{ children }}
</li>
{% endrecursenavigation %}
</ul>
+
+ .. note:: {% recursenavigation %} requires that the current :class:`HttpRequest` be present in the context as ``request``. The simplest way to do this is with the `request context processor`_. Simply make sure that ``django.core.context_processors.request`` is included in your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
+
+ .. _request context processor: https://docs.djangoproject.com/en/dev/ref/templates/api/#django-core-context-processors-request
+
"""
bits = token.contents.split()
if len(bits) != 3:
def has_navigation(node, key=None):
"""Returns ``True`` if the node has a :class:`.Navigation` with the given key and ``False`` otherwise. If ``key`` is ``None``, returns whether the node has any :class:`.Navigation`\ s at all."""
try:
- nav = node.navigation
- if key is not None:
- if key in nav and bool(node.navigation[key]):
- return True
- elif key not in node.navigation:
- return False
- return bool(node.navigation)
+ return bool(node.navigation[key])
except:
return False
def navigation_host(node, key):
"""Returns the :class:`.Node` which hosts the :class:`.Navigation` which ``node`` has inherited for ``key``. Returns ``node`` if any exceptions are encountered."""
try:
- return Navigation.objects.filter(node__in=node.get_ancestors(include_self=True), key=key).order_by('-node__level')[0].node
+ return node.navigation[key].node
except:
return node
\ No newline at end of file
search_fields = ['string', 'result_urls__url']
actions = ['results_action']
if 'grappelli' in settings.INSTALLED_APPS:
- results_template = 'admin/sobol/search/grappelli_results.html'
- else:
- results_template = 'admin/sobol/search/results.html'
-
- def get_urls(self):
- urlpatterns = super(SearchAdmin, self).get_urls()
-
- def wrap(view):
- def wrapper(*args, **kwargs):
- return self.admin_site.admin_view(view)(*args, **kwargs)
- return update_wrapper(wrapper, view)
-
- info = self.model._meta.app_label, self.model._meta.module_name
-
- urlpatterns = patterns('',
- url(r'^results/$', wrap(self.results_view), name="%s_%s_selected_results" % info),
- url(r'^(.+)/results/$', wrap(self.results_view), name="%s_%s_results" % info)
- ) + urlpatterns
- return urlpatterns
+ change_form_template = 'admin/sobol/search/grappelli_change_form.html'
def unique_urls(self, obj):
return obj.unique_urls
def queryset(self, request):
qs = super(SearchAdmin, self).queryset(request)
return qs.annotate(total_clicks=Count('result_urls__clicks', distinct=True), unique_urls=Count('result_urls', distinct=True))
-
- def results_action(self, request, queryset):
- info = self.model._meta.app_label, self.model._meta.module_name
- if len(queryset) == 1:
- return HttpResponseRedirect(reverse("admin:%s_%s_results" % info, args=(queryset[0].pk,)))
- else:
- url = reverse("admin:%s_%s_selected_results" % info)
- return HttpResponseRedirect("%s?ids=%s" % (url, ','.join([str(item.pk) for item in queryset])))
- results_action.short_description = "View results for selected %(verbose_name_plural)s"
-
- def results_view(self, request, object_id=None, extra_context=None):
- if object_id is not None:
- object_ids = [object_id]
- else:
- object_ids = request.GET.get('ids').split(',')
-
- if object_ids is None:
- raise Http404
-
- qs = self.queryset(request).filter(pk__in=object_ids)
- opts = self.model._meta
-
- if len(object_ids) == 1:
- title = _(u"Search results for %s" % qs[0])
- else:
- title = _(u"Search results for multiple objects")
-
- context = {
- 'title': title,
- 'queryset': qs,
- 'opts': opts,
- 'root_path': self.admin_site.root_path,
- 'app_label': opts.app_label
- }
- return render_to_response(self.results_template, context, context_instance=RequestContext(request))
class SearchViewAdmin(EntityAdmin):
get_latest_by = 'datetime'
-class RegistryChoiceField(SlugMultipleChoiceField):
- def _get_choices(self):
- if isinstance(self._choices, RegistryIterator):
- return self._choices.copy()
- elif hasattr(self._choices, 'next'):
- choices, self._choices = itertools.tee(self._choices)
- return choices
- else:
- return self._choices
- choices = property(_get_choices)
-
-
try:
from south.modelsinspector import add_introspection_rules
except ImportError:
"""Handles a view for the results of a search, anonymously tracks the selections made by end users, and provides an AJAX API for asynchronous search result loading. This can be particularly useful if some searches are slow."""
#: :class:`ForeignKey` to a :class:`.Page` which will be used to render the search results.
results_page = models.ForeignKey(Page, related_name='search_results_related')
- #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of the :class:`.SearchRegistry`
- searches = RegistryChoiceField(choices=registry.iterchoices())
+ #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of :obj:`.sobol.search.registry`
+ searches = SlugMultipleChoiceField(choices=registry.iterchoices())
#: A :class:`BooleanField` which controls whether or not the AJAX API is enabled.
#:
#: .. note:: If the AJAX API is enabled, a ``ajax_api_url`` attribute will be added to each search instance containing the url and get parameters for an AJAX request to retrieve results for that search.
return HttpResponse(json.dumps({
'search': search_instance.slug,
'results': [result.get_context() for result in search_instance.results],
- 'rendered': [result.render() for result in search_instance.results],
'hasMoreResults': search_instance.has_more_results,
'moreResultsURL': search_instance.more_results_url,
}), mimetype="application/json")
\ No newline at end of file
from django.utils.http import urlquote_plus
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
-from django.template import loader, Context, Template
+from django.template import loader, Context, Template, TemplateDoesNotExist
-from philo.contrib.sobol.utils import make_tracking_querydict, RegistryIterator
+from philo.contrib.sobol.utils import make_tracking_querydict
+from philo.utils.registry import Registry
if getattr(settings, 'SOBOL_USE_EVENTLET', False):
__all__ = (
- 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'SearchRegistry', 'registry', 'get_search_instance'
+ 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'registry', 'get_search_instance'
)
SEARCH_CACHE_SEED = 'philo_sobol_search_results'
-USE_CACHE = getattr(settings, 'SOBOL_USE_SEARCH', True)
+USE_CACHE = getattr(settings, 'SOBOL_USE_CACHE', True)
-class RegistrationError(Exception):
- """Raised if there is a problem registering a search with a :class:`SearchRegistry`"""
- pass
-
-
-class SearchRegistry(object):
- """Holds a registry of search types by slug."""
-
- def __init__(self):
- self._registry = {}
-
- def register(self, search, slug=None):
- """
- Register a search with the registry.
-
- :param search: The search class to register - generally a subclass of :class:`BaseSearch`
- :param slug: The slug which will be used to register the search class. If ``slug`` is ``None``, the search's default slug will be used.
- :raises: :class:`RegistrationError` if a different search is already registered with ``slug``.
-
- """
- slug = slug or search.slug
- if slug in self._registry:
- registered = self._registry[slug]
- if registered.__module__ != search.__module__:
- raise RegistrationError("A different search is already registered as `%s`" % slug)
- else:
- self._registry[slug] = search
-
- def unregister(self, search, slug=None):
- """
- Unregister a search from the registry.
-
- :param search: The search class to unregister - generally a subclass of :class:`BaseSearch`
- :param slug: If provided, the search will only be removed if it was registered with ``slug``. If not provided, the search class will be unregistered no matter what slug it was registered with.
- :raises: :class:`RegistrationError` if a slug is provided but the search registered with that slug is not ``search``.
-
- """
- if slug is not None:
- if slug in self._registry and self._registry[slug] == search:
- del self._registry[slug]
- raise RegistrationError("`%s` is not registered as `%s`" % (search, slug))
- else:
- for slug, search in self._registry.items():
- if search == search:
- del self._registry[slug]
-
- def items(self):
- """Returns a list of (slug, search) items in the registry."""
- return self._registry.items()
-
- def iteritems(self):
- """Returns an iterator over the (slug, search) pairs in the registry."""
- return RegistryIterator(self._registry, 'iteritems')
-
- def iterchoices(self):
- """Returns an iterator over (slug, search.verbose_name) pairs for the registry."""
- return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1].verbose_name))
-
- def __getitem__(self, key):
- """Returns the search registered with ``key``."""
- return self._registry[key]
-
- def __iter__(self):
- """Returns an iterator over the keys in the registry."""
- return self._registry.__iter__()
-
-
-registry = SearchRegistry()
+#: A registry for :class:`BaseSearch` subclasses that should be available in the admin.
+registry = Registry()
def _make_cache_key(search, search_arg):
instance = search(search_arg)
instance.slug = slug
return instance
-
class Result(object):
return self.search.get_result_title(self.result)
def get_url(self):
- """Returns the url of the result or an empty string by calling :meth:`BaseSearch.get_result_url` on the raw result."""
+ """Returns the url of the result or ``None`` by calling :meth:`BaseSearch.get_result_url` on the raw result. This url will contain a querystring which, if used, will track a :class:`.Click` for the actual url."""
return self.search.get_result_url(self.result)
+ def get_actual_url(self):
+ """Returns the actual url of the result by calling :meth:`BaseSearch.get_actual_result_url` on the raw result."""
+ return self.search.get_actual_result_url(self.result)
+
def get_content(self):
"""Returns the content of the result by calling :meth:`BaseSearch.get_result_content` on the raw result."""
return self.search.get_result_content(self.result)
- def get_extra_context(self):
- """Returns any extra context for the result by calling :meth:`BaseSearch.get_result_extra_context` on the raw result."""
- return self.search.get_result_extra_context(self.result)
-
def get_template(self):
"""Returns the template which will be used to render the :class:`Result` by calling :meth:`BaseSearch.get_result_template` on the raw result."""
return self.search.get_result_template(self.result)
def get_context(self):
"""
- Returns the context dictionary for the result. This is used both in rendering the result and in the AJAX return value for :meth:`.SearchView.ajax_api_view`. The context will contain everything from :meth:`get_extra_context` as well as the following keys:
+ Returns the context dictionary for the result. This is used both in rendering the result and in the AJAX return value for :meth:`.SearchView.ajax_api_view`. The context will contain the following keys:
title
The result of calling :meth:`get_title`
The result of calling :meth:`get_content`
"""
- context = self.get_extra_context()
- context.update({
- 'title': self.get_title(),
- 'url': self.get_url(),
- 'content': self.get_content()
- })
- return context
+ if not hasattr(self, '_context'):
+ self._context = {
+ 'title': self.get_title(),
+ 'url': self.get_url(),
+ 'actual_url': self.get_actual_url(),
+ 'content': self.get_content()
+ }
+ return self._context
def render(self):
"""Returns the template from :meth:`get_template` rendered with the context from :meth:`get_context`."""
result_limit = 5
#: How long the items for the search should be cached (in minutes). Default: 48 hours.
_cache_timeout = 60*48
- #: The path to the template which will be used to render the :class:`Result`\ s for this search. If this is ``None``, then the framework will try "sobol/search/<slug>/result.html" and "sobol/search/result.html".
+ #: The path to the template which will be used to render the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/result.html`` and ``sobol/search/result.html``.
result_template = None
+ #: The path to the template which will be used to generate the title of the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/title.html`` and ``sobol/search/title.html``.
+ title_template = None
+ #: The path to the template which will be used to generate the content of the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/content.html`` and ``sobol/search/content.html``.
+ content_template = None
def __init__(self, search_arg):
self.search_arg = search_arg
self._results = results
if USE_CACHE:
+ for result in results:
+ result.get_context()
key = _make_cache_key(self, self.search_arg)
cache.set(key, self, self._cache_timeout)
"""Returns an iterable of up to ``limit`` results. The :meth:`get_result_title`, :meth:`get_result_url`, :meth:`get_result_template`, and :meth:`get_result_extra_context` methods will be used to interpret the individual items that this function returns, so the result can be an object with attributes as easily as a dictionary with keys. However, keep in mind that the raw results will be stored with django's caching mechanisms and will be converted to JSON."""
raise NotImplementedError
- def get_result_title(self, result):
- """Returns the title of the ``result``. Must be implemented by subclasses."""
- raise NotImplementedError
-
def get_actual_result_url(self, result):
"""Returns the actual URL for the ``result`` or ``None`` if there is no URL. Must be implemented by subclasses."""
raise NotImplementedError
def get_result_querydict(self, result):
"""Returns a querydict for tracking selection of the result, or ``None`` if there is no URL for the result."""
- url = self.get_result_url(result)
+ url = self.get_actual_result_url(result)
if url is None:
return None
return make_tracking_querydict(self.search_arg, url)
return None
return "?%s" % qd.urlencode()
- def get_result_content(self, result):
- """Returns the content for the ``result`` or ``None`` if there is no content. Must be implemented by subclasses."""
- raise NotImplementedError
+ def get_result_title(self, result):
+ """Returns the title of the ``result``. By default, renders ``sobol/search/<slug>/title.html`` or ``sobol/search/title.html`` with the result in the context. This can be overridden by setting :attr:`title_template` or simply overriding :meth:`get_result_title`. If no template can be found, this will raise :exc:`TemplateDoesNotExist`."""
+ return loader.render_to_string(self.title_template or [
+ 'sobol/search/%s/title.html' % self.slug,
+ 'sobol/search/title.html'
+ ], {'result': result})
- def get_result_extra_context(self, result):
- """Returns any extra context to be used when rendering the ``result``. Make sure that any extra context can be serialized as JSON."""
- return {}
+ def get_result_content(self, result):
+ """Returns the content for the ``result``. By default, renders ``sobol/search/<slug>/content.html`` or ``sobol/search/content.html`` with the result in the context. This can be overridden by setting :attr:`content_template` or simply overriding :meth:`get_result_content`. If no template is found, this will return an empty string."""
+ try:
+ return loader.render_to_string(self.content_template or [
+ 'sobol/search/%s/content.html' % self.slug,
+ 'sobol/search/content.html'
+ ], {'result': result})
+ except TemplateDoesNotExist:
+ return ""
def get_result_template(self, result):
"""Returns the template to be used for rendering the ``result``. For a search with slug ``google``, this would first try ``sobol/search/google/result.html``, then fall back on ``sobol/search/result.html``. Subclasses can override this by setting :attr:`result_template` to the path of another template."""
def get_actual_more_results_url(self):
return self._more_results_url
- def get_result_title(self, result):
- return result['titleNoFormatting']
-
- def get_result_url(self, result):
+ def get_actual_result_url(self, result):
return result['unescapedUrl']
+ def get_result_title(self, result):
+ return mark_safe(result['titleNoFormatting'])
+
def get_result_content(self, result):
- return result['content']
+ return mark_safe(result['content'])
registry.register(GoogleSearch)
(function($){
var sobol = window.sobol = {};
+ sobol.favoredResults = []
+ sobol.favoredResultSearch = null;
sobol.search = function(){
var searches = sobol.searches = $('article.search');
- for (var i=0;i<searches.length;i++) {
+ if(sobol.favoredResults.length) sobol.favoredResultSearch = searches.eq(0);
+ for (var i=sobol.favoredResults.length ? 1 : 0;i<searches.length;i++) {
(function(){
var s = searches[i];
$.ajax({
}());
};
}
+ sobol.renderResult = function(result){
+ // Returns the result rendered as a string. Override this to provide custom rendering.
+ var url = result['url'],
+ title = result['title'],
+ content = result['content'],
+ rendered = '';
+
+ if(url){
+ rendered += "<dt><a href='" + url + "'>" + title + "</a></dt>";
+ } else {
+ rendered += "<dt>" + title + "</dt>";
+ }
+ if(content && content != ''){
+ rendered += "<dd>" + content + "</dd>"
+ }
+ return rendered
+ }
+ sobol.addFavoredResult = function(result) {
+ var dl = sobol.favoredResultSearch.find('dl');
+ if(!dl.length){
+ dl = $('<dl>');
+ dl.appendTo(sobol.favoredResultSearch);
+ sobol.favoredResultSearch.removeClass('loading');
+ }
+ dl[0].innerHTML += sobol.renderResult(result)
+ }
sobol.onSuccess = function(ele, data){
// hook for success!
- ele.removeClass('loading')
+ ele.removeClass('loading');
if (data['results'].length) {
- ele[0].innerHTML += "<dl>" + data['rendered'].join("") + "</dl>";
+ ele[0].innerHTML += "<dl>";
+ $.each(data['results'], function(i, v){
+ ele[0].innerHTML += sobol.renderResult(v);
+ })
+ ele[0].innerHTML += "</dl>";
if(data['hasMoreResults'] && data['moreResultsURL']) ele[0].innerHTML += "<footer><p><a href='" + data['moreResultsURL'] + "'>See more results</a></p></footer>";
} else {
ele.addClass('empty');
ele[0].innerHTML += "<p>No results found.</p>";
ele.slideUp();
}
+ if (sobol.favoredResultSearch){
+ for (var i=0;i<data['results'].length;i++){
+ var r = data['results'][i];
+ if ($.inArray(r['actual_url'], sobol.favoredResults) != -1){
+ sobol.addFavoredResult(r);
+ }
+ }
+ }
};
sobol.onError = function(ele, textStatus, errorThrown){
// Hook for error...
.favored td{
font-weight:bold;
}
- #changelist{
- border:none;
- background:none;
+ #changelist table{
+ width:100%;
}
</style>
{% endblock %}
</tbody>
</table>
</div>
- <div class="module footer">
- <ul class="submit-row">
- {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<li class="left delete-link-container"><a href="delete/" class="delete-link">{% trans "Delete" %}</a></li>{% endif %}{% endif %}
- </ul>
+ {% block submit_row %}
+ <div class="submit-row">
+ {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<p class="deletelink-box"><a href="delete/" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}{% endif %}
</div>
+ {% endblock %}
{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends 'admin/sobol/search/change_form.html' %}
+{% load i18n %}
+
+{% block extrastyle %}
+ <style type="text/css">
+ .favored td{
+ font-weight:bold;
+ }
+ #changelist{
+ border:none;
+ background:none;
+ }
+ thead th{color:#444;font-weight:bold;}
+ </style>
+{% endblock %}
+
+{% block submit_row %}
+ <div class="module footer">
+ <ul class="submit-row">
+ {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<li class="left delete-link-container"><a href="delete/" class="delete-link">{% trans "Delete" %}</a></li>{% endif %}{% endif %}
+ </ul>
+ </div>
+{% endblock %}
\ No newline at end of file
+++ /dev/null
-{% extends "admin/base_site.html" %}
-{% load i18n %}
-
-{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
-
-{% block breadcrumbs %}
-<div class="breadcrumbs">
- {% if queryset|length > 1 %}
- <a href="../../">{% trans "Home" %}</a> ›
- <a href="../">{{ app_label|capfirst }}</a> ›
- <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
- {% trans 'Search results for multiple objects' %}
- {% else %}
- <a href="../../../../">{% trans "Home" %}</a> ›
- <a href="../../../">{{ app_label|capfirst }}</a> ›
- <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
- <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
- {% trans 'Results' %}
- {% endif %}
-</div>
-{% endblock %}
-
-
-{% block content %}
- {% for search in queryset %}
- <fieldset class="module">
- <h2>{{ search.string }}</h2>
- <table>
- <thead>
- <tr>
- <th>Weight</th>
- <th>URL</th>
- </tr>
- </thead>
- <tbody>
- {% for result in search.get_weighted_results %}
- <tr{% if result in search.favored_results %} class="favored"{% endif %}>
- <td>{{ result.weight }}</td>
- <td>{{ result.url }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </fieldset>
- {% endfor %}
-{% endblock %}
\ No newline at end of file
}(jQuery));
</script>
{% endif %}
+{% if favored_results %}
+ <article class="search favored{% if ajax %} loading{% endif %}">
+ <header>
+ <h1>Favored results</h1>
+ </header>
+ {% if not ajax %}
+ <dl>
+ {% for search in searches %}
+ {% for result in search.results %}
+ {% if result.get_actual_url in favored_results %}
+ {{ result }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ {% if search.get_actual_more_results_url in favored_results %}
+ <dt><a href="{{ search.more_results_url }}">More results for {{ search }}</a></dt>
+ {% endif %}
+ </dl>
+ {% endif %}
+ </article>
+{% endif %}
{% for search in searches %}
<article {% if ajax %}class="search loading {{ search.slug }}" data-url="{{ search.ajax_api_url }}"{% else %}class="search {{ search.slug }}{% if not search.results %} empty{% endif %}"{% endif %}>
<header>
</dl>
{% if search.has_more_results and search.more_results_url %}
<footer>
- <p><a href="?{{ search.more_results_querydict.urlencode }}">See more results</a></p>
+ <p><a href="{{ search.more_results_url }}">See more results</a></p>
</footer>
{% endif %}
{% else %}
--- /dev/null
+{{ result.content|truncatewords_html:20 }}
\ No newline at end of file
-<dt>{% if url %}<a href="{{ url }}">{% endif %}{{ title|safe }}{% if url %}</a>{% endif %}</dt>
-{% if content %}<dd>{{ content|safe|truncatewords_html:20 }}</dd>{% endif %}
\ No newline at end of file
+<dt>{% if url %}<a href="{{ url }}">{% endif %}{{ title }}{% if url %}</a>{% endif %}</dt>
+{% if content %}<dd>{{ content }}</dd>{% endif %}
\ No newline at end of file
def urlpatterns(self):
urlpatterns = super(PasswordMultiView, self).urlpatterns
- if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
+ if self.password_reset_page_id and self.password_reset_confirmation_email_id and self.password_set_page_id:
urlpatterns += patterns('',
url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
)
- if self.password_change_page:
+ if self.password_change_page_id:
urlpatterns += patterns('',
url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
)
@property
def urlpatterns(self):
urlpatterns = super(RegistrationMultiView, self).urlpatterns
- if self.register_page and self.register_confirmation_email:
+ if self.register_page_id and self.register_confirmation_email_id:
urlpatterns += patterns('',
url(r'^register$', csrf_protect(self.register), name='register'),
url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
@property
def urlpatterns(self):
urlpatterns = super(AccountMultiView, self).urlpatterns
- if self.manage_account_page:
+ if self.manage_account_page_id:
urlpatterns += patterns('',
url(r'^account$', self.login_required(self.account_view), name='account'),
)
- if self.email_change_confirmation_email:
+ if self.email_change_confirmation_email_id:
urlpatterns += patterns('',
url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
)
self.set_requirement_redirect(request, redirect=request.path)
redirect = self.reverse('account', node=request.node)
else:
- redirect = node.get_absolute_url()
+ redirect = request.node.get_absolute_url()
return HttpResponseRedirect(redirect)
return view(request, *args, **kwargs)
--- /dev/null
+"""
+Winer provides the same API as `django's syndication Feed class <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.django.contrib.syndication.views.Feed>`_, adapted to a Philo-style :class:`~philo.models.nodes.MultiView` for easy database management. Apps that need syndication can simply subclass :class:`~philo.contrib.winer.models.FeedView`, override a few methods, and start serving RSS and Atom feeds. See :class:`~philo.contrib.penfield.models.BlogView` for a concrete implementation example.
+
+"""
\ No newline at end of file
--- /dev/null
+class HttpNotAcceptable(Exception):
+ """This will be raised in :meth:`.FeedView.get_feed_type` if an Http-Accept header will not accept any of the feed content types that are available."""
+ pass
\ No newline at end of file
--- /dev/null
+from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+
+from philo.utils.registry import Registry
+
+
+DEFAULT_FEED = Atom1Feed
+
+
+registry = Registry()
+
+
+registry.register(Atom1Feed, verbose_name='Atom')
+registry.register(Rss201rev2Feed, verbose_name='RSS')
\ No newline at end of file
from django.http import HttpResponse
from django.utils.decorators import decorator_from_middleware
-from philo.contrib.penfield.exceptions import HttpNotAcceptable
+from philo.contrib.winer.exceptions import HttpNotAcceptable
class HttpNotAcceptableMiddleware(object):
- """Middleware to catch :exc:`~philo.contrib.penfield.exceptions.HttpNotAcceptable` and return an :class:`HttpResponse` with a 406 response code. See :rfc:`2616`."""
+ """Middleware to catch :exc:`~philo.contrib.winer.exceptions.HttpNotAcceptable` and return an :class:`HttpResponse` with a 406 response code. See :rfc:`2616`."""
def process_exception(self, request, exception):
if isinstance(exception, HttpNotAcceptable):
return HttpResponse(status=406)
--- /dev/null
+from django.conf import settings
+from django.conf.urls.defaults import url, patterns, include
+from django.contrib.sites.models import Site, RequestSite
+from django.contrib.syndication.views import add_domain
+from django.db import models
+from django.http import HttpResponse
+from django.template import RequestContext, Template as DjangoTemplate
+from django.utils import feedgenerator, tzinfo
+from django.utils.encoding import smart_unicode, force_unicode
+from django.utils.html import escape
+
+from philo.contrib.winer.exceptions import HttpNotAcceptable
+from philo.contrib.winer.feeds import registry, DEFAULT_FEED
+from philo.contrib.winer.middleware import http_not_acceptable
+from philo.models import Page, Template, MultiView
+
+try:
+ import mimeparse
+except:
+ mimeparse = None
+
+
+class FeedView(MultiView):
+ """
+ :class:`FeedView` is an abstract model which handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.django.contrib.syndication.views.Feed>`_.
+
+ """
+ #: The type of feed which should be served by the :class:`FeedView`.
+ feed_type = models.CharField(max_length=50, choices=registry.choices, default=registry.get_slug(DEFAULT_FEED))
+ #: The suffix which will be appended to a page URL for a :attr:`feed_type` feed of its items. Default: "feed". Note that RSS and Atom feeds will always be available at ``<page_url>/rss`` and ``<page_url>/atom`` regardless of the value of this setting.
+ #:
+ #: .. seealso:: :meth:`get_feed_type`, :meth:`feed_patterns`
+ feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
+ #: A :class:`BooleanField` - whether or not feeds are enabled.
+ feeds_enabled = models.BooleanField(default=True)
+ #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15.
+ feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
+
+ #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
+ item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
+ #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
+ item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
+
+ #: An attribute holding the name of the context variable to be populated with the items managed by the :class:`FeedView`. Default: "items"
+ item_context_var = 'items'
+ #: An attribute holding the name of the attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`~philo.contrib.penfield.models.Blog`.) Default: "object"
+ #:
+ #: Example::
+ #:
+ #: class BlogView(FeedView):
+ #: blog = models.ForeignKey(Blog)
+ #:
+ #: object_attr = 'blog'
+ #: item_context_var = 'entries'
+ object_attr = 'object'
+
+ #: An attribute holding a description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
+ description = ""
+
+ def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
+ """
+ Given the name to be used to reverse this view and the names of the attributes for the function that fetches the objects, returns patterns suitable for inclusion in urlpatterns. In addition to ``base`` (which will serve the page at ``page_attr``) and ``base`` + :attr:`feed_suffix` (which will serve a :attr:`feed_type` feed), patterns will be provided for each registered feed type as ``base`` + ``slug``.
+
+ :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath.
+ :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`.
+ :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``.
+ :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
+ :returns: Patterns suitable for use in urlpatterns.
+
+ Example::
+
+ class BlogView(FeedView):
+ blog = models.ForeignKey(Blog)
+ entry_archive_page = models.ForeignKey(Page)
+
+ @property
+ def urlpatterns(self):
+ urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
+ return urlpatterns
+
+ def get_entries_by_ymd(request, year, month, day, extra_context=None):
+ entries = Blog.entries.all()
+ # filter entries based on the year, month, and day.
+ return entries, extra_context
+
+ .. seealso:: :meth:`get_feed_type`
+
+ """
+ feed_patterns = ()
+ if self.feeds_enabled:
+ suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry]
+ for suffix, feed_type in suffixes:
+ feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type))
+ feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix)
+ feed_patterns += (url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),)
+ feed_patterns += (url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name),)
+ return patterns('', *feed_patterns)
+
+ def get_object(self, request, **kwargs):
+ """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
+ return getattr(self, self.object_attr)
+
+ def feed_view(self, get_items_attr, reverse_name, feed_type=None):
+ """
+ Returns a view function that renders a list of items as a feed.
+
+ :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with the object for the feed and view arguments.
+ :param reverse_name: The name which can be used reverse the page for this feed using the :class:`FeedView` as the urlconf.
+ :param feed_type: The slug used to render the feed class which will be used by the returned view function.
+
+ :returns: A view function that renders a list of items as a feed.
+
+ """
+ get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
+
+ def inner(request, extra_context=None, *args, **kwargs):
+ obj = self.get_object(request, *args, **kwargs)
+ feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs)
+ items, xxx = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
+ self.populate_feed(feed, items, request)
+
+ response = HttpResponse(mimetype=feed.mime_type)
+ feed.write(response, 'utf-8')
+ return response
+
+ return inner
+
+ def page_view(self, get_items_attr, page_attr):
+ """
+ :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
+ :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``.
+
+ :returns: A view function that renders a list of items as an :class:`HttpResponse`.
+
+ """
+ get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
+ page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
+
+ def inner(request, extra_context=None, *args, **kwargs):
+ obj = self.get_object(request, *args, **kwargs)
+ items, extra_context = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
+ items, item_context = self.process_page_items(request, items)
+
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update(item_context or {})
+
+ return page.render_to_response(request, extra_context=context)
+ return inner
+
+ def process_page_items(self, request, items):
+ """
+ Hook for handling any extra processing of ``items`` based on an :class:`HttpRequest`, such as pagination or searching. This method is expected to return a list of items and a dictionary to be added to the page context.
+
+ """
+ item_context = {
+ self.item_context_var: items
+ }
+ return items, item_context
+
+ def get_feed_type(self, request, feed_type=None):
+ """
+ If ``feed_type`` is not ``None``, returns the corresponding class from the registry or raises :exc:`.HttpNotAcceptable`.
+
+ Otherwise, intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, raises :exc:`.HttpNotAcceptable`.
+
+ If `mimeparse <http://code.google.com/p/mimeparse/>`_ is installed, it will be used to select the best matching accepted format; otherwise, the first available format that is accepted will be selected.
+
+ """
+ if feed_type is not None:
+ feed_type = registry[feed_type]
+ loose = False
+ else:
+ feed_type = registry.get(self.feed_type, DEFAULT_FEED)
+ loose = True
+ mt = feed_type.mime_type
+ accept = request.META.get('HTTP_ACCEPT')
+ if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
+ # Wups! They aren't accepting the chosen format.
+ feed_type = None
+ if loose:
+ # Is there another format we can use?
+ accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
+ if mimeparse:
+ mt = mimeparse.best_match(accepted_mts.keys(), accept)
+ if mt:
+ feed_type = accepted_mts[mt]
+ else:
+ for mt in accepted_mts:
+ if mt in accept or "%s/*" % mt.split("/")[0] in accept:
+ feed_type = accepted_mts[mt]
+ break
+ if not feed_type:
+ raise HttpNotAcceptable
+ return feed_type
+
+ def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs):
+ """
+ Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
+
+ :param obj: The object for which the feed should be generated.
+ :param request: The current request.
+ :param reverse_name: The name which can be used to reverse the URL of the page corresponding to this feed.
+ :param feed_type: The slug used to register the feed class that will be instantiated and returned.
+
+ :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``.
+
+ """
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = RequestSite(request)
+
+ feed_type = self.get_feed_type(request, feed_type)
+ node = request.node
+ link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure())
+
+ feed = feed_type(
+ title = self.__get_dynamic_attr('title', obj),
+ subtitle = self.__get_dynamic_attr('subtitle', obj),
+ link = link,
+ description = self.__get_dynamic_attr('description', obj),
+ language = settings.LANGUAGE_CODE.decode(),
+ feed_url = add_domain(
+ current_site.domain,
+ self.__get_dynamic_attr('feed_url', obj) or node.construct_url(self.reverse("%s_%s" % (reverse_name, registry.get_slug(feed_type)), args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure()),
+ request.is_secure()
+ ),
+ author_name = self.__get_dynamic_attr('author_name', obj),
+ author_link = self.__get_dynamic_attr('author_link', obj),
+ author_email = self.__get_dynamic_attr('author_email', obj),
+ categories = self.__get_dynamic_attr('categories', obj),
+ feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
+ feed_guid = self.__get_dynamic_attr('feed_guid', obj),
+ ttl = self.__get_dynamic_attr('ttl', obj),
+ **self.feed_extra_kwargs(obj)
+ )
+ return feed
+
+ def populate_feed(self, feed, items, request):
+ """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
+ if self.item_title_template:
+ title_template = DjangoTemplate(self.item_title_template.code)
+ else:
+ title_template = None
+ if self.item_description_template:
+ description_template = DjangoTemplate(self.item_description_template.code)
+ else:
+ description_template = None
+
+ node = request.node
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = RequestSite(request)
+
+ if self.feed_length is not None:
+ items = items[:self.feed_length]
+
+ for item in items:
+ if title_template is not None:
+ title = title_template.render(RequestContext(request, {'obj': item}))
+ else:
+ title = self.__get_dynamic_attr('item_title', item)
+ if description_template is not None:
+ description = description_template.render(RequestContext(request, {'obj': item}))
+ else:
+ description = self.__get_dynamic_attr('item_description', item)
+
+ link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
+
+ enc = None
+ enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
+ if enc_url:
+ enc = feedgenerator.Enclosure(
+ url = smart_unicode(add_domain(
+ current_site.domain,
+ enc_url,
+ request.is_secure()
+ )),
+ length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
+ mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
+ )
+ author_name = self.__get_dynamic_attr('item_author_name', item)
+ if author_name is not None:
+ author_email = self.__get_dynamic_attr('item_author_email', item)
+ author_link = self.__get_dynamic_attr('item_author_link', item)
+ else:
+ author_email = author_link = None
+
+ pubdate = self.__get_dynamic_attr('item_pubdate', item)
+ if pubdate and not pubdate.tzinfo:
+ ltz = tzinfo.LocalTimezone(pubdate)
+ pubdate = pubdate.replace(tzinfo=ltz)
+
+ feed.add_item(
+ title = title,
+ link = link,
+ description = description,
+ unique_id = self.__get_dynamic_attr('item_guid', item, link),
+ enclosure = enc,
+ pubdate = pubdate,
+ author_name = author_name,
+ author_email = author_email,
+ author_link = author_link,
+ categories = self.__get_dynamic_attr('item_categories', item),
+ item_copyright = self.__get_dynamic_attr('item_copyright', item),
+ **self.item_extra_kwargs(item)
+ )
+
+ def __get_dynamic_attr(self, attname, obj, default=None):
+ try:
+ attr = getattr(self, attname)
+ except AttributeError:
+ return default
+ if callable(attr):
+ # Check func_code.co_argcount rather than try/excepting the
+ # function and catching the TypeError, because something inside
+ # the function may raise the TypeError. This technique is more
+ # accurate.
+ if hasattr(attr, 'func_code'):
+ argcount = attr.func_code.co_argcount
+ else:
+ argcount = attr.__call__.func_code.co_argcount
+ if argcount == 2: # one argument is 'self'
+ return attr(obj)
+ else:
+ return attr()
+ return attr
+
+ def feed_extra_kwargs(self, obj):
+ """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
+ return {}
+
+ def item_extra_kwargs(self, item):
+ """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
+ return {}
+
+ def item_title(self, item):
+ return escape(force_unicode(item))
+
+ def item_description(self, item):
+ return force_unicode(item)
+
+ class Meta:
+ abstract=True
\ No newline at end of file
class RequestNodeMiddleware(object):
- """Adds a ``node`` attribute, representing the currently-viewed node, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`."""
+ """
+ Adds a ``node`` attribute, representing the currently-viewed :class:`.Node`, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`.
+
+ :class:`RequestNodeMiddleware` also catches all exceptions raised while handling requests that have attached :class:`.Node`\ s if :setting:`settings.DEBUG` is ``True``. If a :exc:`django.http.Http404` error was caught, :class:`RequestNodeMiddleware` will look for an "Http404" :class:`.Attribute` on the request's :class:`.Node`; otherwise it will look for an "Http500" :class:`.Attribute`. If an appropriate :class:`.Attribute` is found, and the value of the attribute is a :class:`.View` instance, then the :class:`.View` will be rendered with the exception in the ``extra_context``, bypassing any later handling of exceptions.
+
+ """
def process_view(self, request, view_func, view_args, view_kwargs):
try:
path = view_kwargs['path']
if isinstance(exception, Http404):
error_view = request.node.attributes.get('Http404', None)
+ status_code = 404
else:
error_view = request.node.attributes.get('Http500', None)
+ status_code = 500
if error_view is None or not isinstance(error_view, View):
# Should this be duck-typing? Perhaps even no testing?
return
extra_context = {'exception': exception}
- return error_view.render_to_response(request, extra_context)
\ No newline at end of file
+ response = error_view.render_to_response(request, extra_context)
+ response.status_code = status_code
+ return response
\ No newline at end of file
--- /dev/null
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Changing field 'Node.view_object_id'
+ db.alter_column('philo_node', 'view_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True))
+
+ # Changing field 'Node.view_content_type'
+ db.alter_column('philo_node', 'view_content_type_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['contenttypes.ContentType']))
+
+
+ def backwards(self, orm):
+
+ # User chose to not deal with backwards NULL issues for 'Node.view_object_id'
+ raise RuntimeError("Cannot reverse this migration. 'Node.view_object_id' and its values cannot be restored.")
+
+ # User chose to not deal with backwards NULL issues for 'Node.view_content_type'
+ raise RuntimeError("Cannot reverse this migration. 'Node.view_content_type' and its values cannot be restored.")
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['philo']
objects = TreeEntityManager()
parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
- def get_path(self, root=None, pathsep='/', field='slug'):
+ def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
"""
:param root: Only return the path since this object.
:param pathsep: The path separator to use when constructing an instance's path
:param field: The field to pull path information from for each ancestor.
+ :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
:returns: A string representation of an object's path.
"""
if root == self:
return ''
- if root is None and self.is_root_node():
+ parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
+ if getattr(root, 'pk', None) == parent_id:
return getattr(self, field, '?')
if root is not None and not self.is_descendant_of(root):
raise AncestorDoesNotExist(root)
+ if memoize:
+ memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
+ try:
+ return self._path_memo[memo_args]
+ except AttributeError:
+ self._path_memo = {}
+ except KeyError:
+ pass
+
qs = self.get_ancestors(include_self=True)
if root is not None:
qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
- return pathsep.join([getattr(parent, field, '?') for parent in qs])
+ path = pathsep.join([getattr(parent, field, '?') for parent in qs])
+
+ if memoize:
+ self._path_memo[memo_args] = path
+
+ return path
path = property(get_path)
def get_attribute_mapper(self, mapper=None):
"""
if mapper is None:
- if self.parent:
+ if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
mapper = TreeAttributeMapper
else:
mapper = AttributeMapper
objects = SlugTreeEntityManager()
slug = models.SlugField(max_length=255)
- def get_path(self, root=None, pathsep='/', field='slug'):
- return super(SlugTreeEntity, self).get_path(root, pathsep, field)
+ def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
+ return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
path = property(get_path)
def clean(self):
- if self.parent is None:
+ if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
try:
self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
except self.DoesNotExist:
from django.utils.translation import ugettext_lazy as _
from philo.forms.fields import JSONFormField
+from philo.utils.registry import RegistryIterator
from philo.validators import TemplateValidator, json_validator
from philo.forms.widgets import EmbedWidget
#from philo.models.fields.entities import *
class SlugMultipleChoiceField(models.Field):
- """Stores a selection of multiple items with unique slugs in the form of a comma-separated list."""
+ """Stores a selection of multiple items with unique slugs in the form of a comma-separated list. Also knows how to correctly handle :class:`RegistryIterator`\ s passed in as choices."""
__metaclass__ = models.SubfieldBase
description = _("Comma-separated slug field")
if invalid_values:
# should really make a custom message.
raise ValidationError(self.error_messages['invalid_choice'] % invalid_values)
+
+ def _get_choices(self):
+ if isinstance(self._choices, RegistryIterator):
+ return self._choices.copy()
+ elif hasattr(self._choices, 'next'):
+ choices, self._choices = itertools.tee(self._choices)
+ return choices
+ else:
+ return self._choices
+ choices = property(_get_choices)
try:
import mimetypes
from os.path import basename
+from django.conf import settings
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site, RequestSite
+from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.servers.basehttp import FileWrapper
from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
_view_content_type_limiter = ContentTypeSubclassLimiter(None)
+CACHE_PHILO_ROOT = getattr(settings, "PHILO_CACHE_PHILO_ROOT", True)
class Node(SlugTreeEntity):
:class:`Node`\ s are the basic building blocks of a website using Philo. They define the URL hierarchy and connect each URL to a :class:`View` subclass instance which is used to generate an HttpResponse.
"""
- view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
- view_object_id = models.PositiveIntegerField()
+ view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter, blank=True, null=True)
+ view_object_id = models.PositiveIntegerField(blank=True, null=True)
#: :class:`GenericForeignKey` to a non-abstract subclass of :class:`View`
view = generic.GenericForeignKey('view_content_type', 'view_object_id')
@property
def accepts_subpath(self):
"""A property shortcut for :attr:`self.view.accepts_subpath <View.accepts_subpath>`"""
- if self.view:
- return self.view.accepts_subpath
+ if self.view_object_id and self.view_content_type_id:
+ return ContentType.objects.get_for_id(self.view_content_type_id).model_class().accepts_subpath
return False
def handles_subpath(self, subpath):
- return self.view.handles_subpath(subpath)
+ if self.view_object_id and self.view_content_type_id:
+ return ContentType.objects.get_for_id(self.view_content_type_id).model_class().handles_subpath(subpath)
+ return False
def render_to_response(self, request, extra_context=None):
"""This is a shortcut method for :meth:`View.render_to_response`"""
- return self.view.render_to_response(request, extra_context)
+ if self.view_object_id and self.view_content_type_id:
+ view_model = ContentType.objects.get_for_id(self.view_content_type_id).model_class()
+ self.view = view_model._default_manager.select_related(depth=1).get(pk=self.view_object_id)
+ return self.view.render_to_response(request, extra_context)
+ raise Http404
def get_absolute_url(self, request=None, with_domain=False, secure=False):
"""
Node urls will not contain a trailing slash unless a subpath is provided which ends with a trailing slash. Subpaths are expected to begin with a slash, as if returned by :func:`django.core.urlresolvers.reverse`.
+ Because this method will be called frequently and will always try to reverse ``philo-root``, the results of that reversal will be cached by default. This can be disabled by setting :setting:`PHILO_CACHE_PHILO_ROOT` to ``False``.
+
:meth:`construct_url` may raise the following exceptions:
- :class:`NoReverseMatch` if "philo-root" is not reversable -- for example, if :mod:`philo.urls` is not included anywhere in your urlpatterns.
"""
# Try reversing philo-root first, since we can't do anything if that fails.
- root_url = reverse('philo-root')
+ if CACHE_PHILO_ROOT:
+ key = "CACHE_PHILO_ROOT__" + settings.ROOT_URLCONF
+ root_url = cache.get(key)
+ if root_url is None:
+ root_url = reverse('philo-root')
+ cache.set(key, root_url)
+ else:
+ root_url = reverse('philo-root')
try:
current_site = Site.objects.get_current()
#: A generic relation back to nodes.
nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
- #: Property or attribute which defines whether this :class:`View` can handle subpaths. Default: ``False``
+ #: An attribute on the class which defines whether this :class:`View` can handle subpaths. Default: ``False``
accepts_subpath = False
- def handles_subpath(self, subpath):
+ @classmethod
+ def handles_subpath(cls, subpath):
"""Returns True if the :class:`View` handles the given subpath, and False otherwise."""
- if not self.accepts_subpath and subpath != "/":
+ if not cls.accepts_subpath and subpath != "/":
return False
return True
"""Returns urlpatterns that point to views (generally methods on the class). :class:`MultiView`\ s can be thought of as "managing" these subpaths."""
raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
- def handles_subpath(self, subpath):
- if not super(MultiView, self).handles_subpath(subpath):
- return False
- try:
- resolve(subpath, urlconf=self)
- except Http404:
- return False
- return True
-
def actually_render_to_response(self, request, extra_context=None):
"""
Resolves the remaining subpath left after finding this :class:`View`'s node using :attr:`self.urlpatterns <urlpatterns>` and renders the view function (or method) found with the appropriate args and kwargs.
kwargs = dict([(smart_str(k, 'ascii'), v) for k, v in params.items()])
return self.url_or_subpath, args, kwargs
- def get_target_url(self):
- """Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`."""
+ def get_target_url(self, memoize=True):
+ """Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`. The results will be memoized by default; this can be prevented by passing in ``memoize=False``."""
+ if memoize:
+ memo_args = (self.target_node_id, self.url_or_subpath, self.reversing_parameters_json)
+ try:
+ return self._target_url_memo[memo_args]
+ except AttributeError:
+ self._target_url_memo = {}
+ except KeyError:
+ pass
+
node = self.target_node
if node is not None and node.accepts_subpath and self.url_or_subpath:
if self.reversing_parameters is not None:
subpath = self.url_or_subpath
if subpath[0] != '/':
subpath = '/' + subpath
- return node.construct_url(subpath)
+ target_url = node.construct_url(subpath)
elif node is not None:
- return node.get_absolute_url()
+ target_url = node.get_absolute_url()
else:
if self.reversing_parameters is not None:
view_name, args, kwargs = self.get_reverse_params()
- return reverse(view_name, args=args, kwargs=kwargs)
- return self.url_or_subpath
+ target_url = reverse(view_name, args=args, kwargs=kwargs)
+ else:
+ target_url = self.url_or_subpath
+
+ if memoize:
+ self._target_url_memo[memo_args] = target_url
+ return target_url
target_url = property(get_target_url)
class Meta:
"""
-import itertools
-
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpResponse
-from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, TextNode, VariableNode
-from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
-from django.utils.datastructures import SortedDict
+from django.template import Context, RequestContext, Template as DjangoTemplate
from philo.models.base import SlugTreeEntity, register_value_model
from philo.models.fields import TemplateField
from philo.models.nodes import View
from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
-from philo.templatetags.containers import ContainerNode
-from philo.utils import fattr
-from philo.validators import LOADED_TEMPLATE_ATTR
+from philo.utils import templates
__all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
-class LazyContainerFinder(object):
- def __init__(self, nodes, extends=False):
- self.nodes = nodes
- self.initialized = False
- self.contentlet_specs = []
- self.contentreference_specs = SortedDict()
- self.blocks = {}
- self.block_super = False
- self.extends = extends
-
- def process(self, nodelist):
- for node in nodelist:
- if self.extends:
- if isinstance(node, BlockNode):
- self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
- block.initialize()
- self.blocks.update(block.blocks)
- continue
-
- if isinstance(node, ContainerNode):
- if not node.references:
- self.contentlet_specs.append(node.name)
- else:
- if node.name not in self.contentreference_specs.keys():
- self.contentreference_specs[node.name] = node.references
- continue
-
- if isinstance(node, VariableNode):
- if node.filter_expression.var.lookups == (u'block', u'super'):
- self.block_super = True
-
- if hasattr(node, 'child_nodelists'):
- for nodelist_name in node.child_nodelists:
- if hasattr(node, nodelist_name):
- nodelist = getattr(node, nodelist_name)
- self.process(nodelist)
-
- # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
- # node as rendering an additional template. Philo monkeypatches the attribute onto
- # the relevant default nodes and declares it on any native nodes.
- if hasattr(node, LOADED_TEMPLATE_ATTR):
- loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
- if loaded_template:
- nodelist = loaded_template.nodelist
- self.process(nodelist)
-
- def initialize(self):
- if not self.initialized:
- self.process(self.nodes)
- self.initialized = True
-
-
-def build_extension_tree(nodelist):
- nodelists = []
- extends = None
- for node in nodelist:
- if not isinstance(node, TextNode):
- if isinstance(node, ExtendsNode):
- extends = node
- break
-
- if extends:
- if extends.nodelist:
- nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
- loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
- nodelists.extend(build_extension_tree(loaded_template.nodelist))
- else:
- # Base case: root.
- nodelists.append(LazyContainerFinder(nodelist))
- return nodelists
-
-
class Template(SlugTreeEntity):
"""Represents a database-driven django template."""
#: The name of the template. Used for organization and debugging.
#: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
code = TemplateField(secure=False, verbose_name='django template code')
- @property
- def containers(self):
+ def get_containers(self):
"""
Returns a tuple where the first item is a list of names of contentlets referenced by containers, and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers. This will break if there is a recursive extends or includes in the template code. Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
"""
template = DjangoTemplate(self.code)
-
- # Build a tree of the templates we're using, placing the root template first.
- levels = build_extension_tree(template.nodelist)
-
- contentlet_specs = []
- contentreference_specs = SortedDict()
- blocks = {}
-
- for level in reversed(levels):
- level.initialize()
- contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs))
- contentreference_specs.update(level.contentreference_specs)
- for name, block in level.blocks.items():
- if block.block_super:
- blocks.setdefault(name, []).append(block)
- else:
- blocks[name] = [block]
-
- for block_list in blocks.values():
- for block in block_list:
- block.initialize()
- contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs))
- contentreference_specs.update(block.contentreference_specs)
-
- return contentlet_specs, contentreference_specs
+ return templates.get_containers(template)
+ containers = property(get_containers)
def __unicode__(self):
"""Returns the value of the :attr:`name` field."""
try:
app_label, model = params[3].strip('"').split('.')
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
except ValueError:
raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag)
except ContentType.DoesNotExist:
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q
from django.utils.safestring import SafeUnicode, mark_safe
register = template.Library()
+CONTAINER_CONTEXT_KEY = 'philo_container_context'
+
+
+class ContainerContext(object):
+ def __init__(self, page):
+ self.page = page
+
+ def get_contentlets(self):
+ if not hasattr(self, '_contentlets'):
+ self._contentlets = dict(((c.name, c) for c in self.page.contentlets.all()))
+ return self._contentlets
+
+ def get_references(self):
+ if not hasattr(self, '_references'):
+ references = self.page.contentreferences.all()
+ self._references = dict((((c.name, ContentType.objects.get_for_id(c.content_type_id)), c) for c in references))
+ return self._references
+
+
class ContainerNode(template.Node):
def __init__(self, name, references=None, as_var=None):
self.name = name
self.references = references
def render(self, context):
- content = settings.TEMPLATE_STRING_IF_INVALID
- if 'page' in context:
- container_content = self.get_container_content(context)
- else:
- container_content = None
+ container_content = self.get_container_content(context)
if self.as_var:
context[self.as_var] = container_content
return ''
- if not container_content:
- return ''
-
return container_content
def get_container_content(self, context):
- page = context['page']
+ try:
+ container_context = context.render_context[CONTAINER_CONTEXT_KEY]
+ except KeyError:
+ try:
+ page = context['page']
+ except KeyError:
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+ container_context = ContainerContext(page)
+ context.render_context[CONTAINER_CONTEXT_KEY] = container_context
+
if self.references:
# Then it's a content reference.
try:
- contentreference = page.contentreferences.get(name__exact=self.name, content_type=self.references)
- content = contentreference.content
- except ObjectDoesNotExist:
+ contentreference = container_context.get_references()[(self.name, self.references)]
+ except KeyError:
content = ''
+ else:
+ content = contentreference.content
else:
# Otherwise it's a contentlet.
try:
- contentlet = page.contentlets.get(name__exact=self.name)
- if '{%' in contentlet.content or '{{' in contentlet.content:
- try:
- content = template.Template(contentlet.content, name=contentlet.name).render(context)
- except template.TemplateSyntaxError, error:
- if settings.DEBUG:
- content = ('[Error parsing contentlet \'%s\': %s]' % (self.name, error))
- else:
- content = settings.TEMPLATE_STRING_IF_INVALID
- else:
- content = contentlet.content
- except ObjectDoesNotExist:
- content = settings.TEMPLATE_STRING_IF_INVALID
- content = mark_safe(content)
+ contentlet = container_context.get_contentlets()[self.name]
+ except KeyError:
+ content = ''
+ else:
+ content = contentlet.content
return content
if option_token == 'references':
try:
app_label, model = remaining_tokens.pop(0).strip('"').split('.')
- references = ContentType.objects.get(app_label=app_label, model=model)
+ references = ContentType.objects.get_by_natural_key(app_label, model)
except IndexError:
raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument specifying a content type' % tag)
except ValueError:
from django.contrib.contenttypes.models import ContentType
from django.template.loader_tags import ExtendsNode, BlockContext, BLOCK_CONTEXT_KEY, TextNode, BlockNode
-from philo.utils import LOADED_TEMPLATE_ATTR
+from philo.utils.templates import LOADED_TEMPLATE_ATTR
register = template.Library()
except ValueError:
raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname)
return ct
if self.with_obj is None and self.view_name is None:
url = node.get_absolute_url()
else:
- if not node.view.accepts_subpath:
+ if not node.accepts_subpath:
return settings.TEMPLATE_STRING_IF_INVALID
if self.with_obj is not None:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, EmptyPage
-from django.template import Context
-from django.template.loader_tags import ExtendsNode, ConstantIncludeNode
def fattr(*args, **kwargs):
else:
objects = page.object_list
- return paginator, page, objects
-
-
-### Facilitating template analysis.
-
-
-LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
-BLANK_CONTEXT = Context()
-
-
-def get_extended(self):
- return self.get_parent(BLANK_CONTEXT)
-
-
-def get_included(self):
- return self.template
-
-
-# We ignore the IncludeNode because it will never work in a blank context.
-setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
-setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
\ No newline at end of file
+ return paginator, page, objects
\ No newline at end of file
value_lookups = {}
for a in attributes:
- value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
+ value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
self._attributes_cache[a.key] = a
values_bulk = {}
- for ct, pks in value_lookups.items():
- values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
+ for ct_pk, pks in value_lookups.items():
+ values_bulk[ct_pk] = ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk(pks)
- self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes]))
+ self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type_id].get(a.value_object_id), 'value', None)) for a in attributes]))
self._cache_filled = True
def clear_cache(self):
--- /dev/null
+from django.core.validators import slug_re
+from django.template.defaultfilters import slugify
+from django.utils.encoding import smart_str
+
+
+class RegistryIterator(object):
+ """
+ Wraps the iterator returned by calling ``getattr(registry, iterattr)`` to provide late instantiation of the wrapped iterator and to allow copying of the iterator for even later instantiation.
+
+ :param registry: The object which provides the iterator at ``iterattr``.
+ :param iterattr: The name of the method on ``registry`` that provides the iterator.
+ :param transform: A function which will be called on each result from the wrapped iterator before it is returned.
+
+ """
+ def __init__(self, registry, iterattr='__iter__', transform=lambda x:x):
+ if not hasattr(registry, iterattr):
+ raise AttributeError("Registry has no attribute %s" % iterattr)
+ self.registry = registry
+ self.iterattr = iterattr
+ self.transform = transform
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if not hasattr(self, '_iter'):
+ self._iter = getattr(self.registry, self.iterattr)()
+
+ return self.transform(self._iter.next())
+
+ def copy(self):
+ """Returns a fresh copy of this iterator."""
+ return self.__class__(self.registry, self.iterattr, self.transform)
+
+
+class RegistrationError(Exception):
+ """Raised if there is a problem registering a object with a :class:`Registry`"""
+ pass
+
+
+class Registry(object):
+ """Holds a registry of arbitrary objects by slug."""
+
+ def __init__(self):
+ self._registry = {}
+
+ def register(self, obj, slug=None, verbose_name=None):
+ """
+ Register an object with the registry.
+
+ :param obj: The object to register.
+ :param slug: The slug which will be used to register the object. If ``slug`` is ``None``, it will be generated from ``verbose_name`` or looked for at ``obj.slug``.
+ :param verbose_name: The verbose name for the object. If ``verbose_name`` is ``None``, it will be looked for at ``obj.verbose_name``.
+ :raises: :class:`RegistrationError` if a different object is already registered with ``slug``, or if ``slug`` is not a valid slug.
+
+ """
+ verbose_name = verbose_name if verbose_name is not None else obj.verbose_name
+
+ if slug is None:
+ slug = getattr(obj, 'slug', slugify(verbose_name))
+ slug = smart_str(slug)
+
+ if not slug_re.search(slug):
+ raise RegistrationError(u"%s is not a valid slug." % slug)
+
+
+ if slug in self._registry:
+ reg = self._registry[slug]
+ if reg['obj'] != obj:
+ raise RegistrationError(u"A different object is already registered as `%s`" % slug)
+ else:
+ self._registry[slug] = {
+ 'obj': obj,
+ 'verbose_name': verbose_name
+ }
+
+ def unregister(self, obj, slug=None):
+ """
+ Unregister an object from the registry.
+
+ :param obj: The object to unregister.
+ :param slug: If provided, the object will only be removed if it was registered with ``slug``. If not provided, the object will be unregistered no matter what slug it was registered with.
+ :raises: :class:`RegistrationError` if ``slug`` is provided and an object other than ``obj`` is registered as ``slug``.
+
+ """
+ if slug is not None:
+ if slug in self._registry:
+ if self._registry[slug]['obj'] == obj:
+ del self._registry[slug]
+ else:
+ raise RegistrationError(u"`%s` is not registered as `%s`" % (obj, slug))
+ else:
+ for slug, reg in self.items():
+ if obj == reg:
+ del self._registry[slug]
+
+ def items(self):
+ """Returns a list of (slug, obj) items in the registry."""
+ return [(slug, self[slug]) for slug in self._registry]
+
+ def values(self):
+ """Returns a list of objects in the registry."""
+ return [self[slug] for slug in self._registry]
+
+ def iteritems(self):
+ """Returns a :class:`RegistryIterator` over the (slug, obj) pairs in the registry."""
+ return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['obj']))
+
+ def itervalues(self):
+ """Returns a :class:`RegistryIterator` over the objects in the registry."""
+ return RegistryIterator(self._registry, 'itervalues', lambda x: x['obj'])
+
+ def iterchoices(self):
+ """Returns a :class:`RegistryIterator` over (slug, verbose_name) pairs for the registry."""
+ return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['verbose_name']))
+ choices = property(iterchoices)
+
+ def get(self, key, default=None):
+ """Returns the object registered with ``key`` or ``default`` if no object was registered."""
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def get_slug(self, obj, default=None):
+ """Returns the slug used to register ``obj`` or ``default`` if ``obj`` was not registered."""
+ for slug, reg in self.iteritems():
+ if obj == reg:
+ return slug
+ return default
+
+ def __getitem__(self, key):
+ """Returns the obj registered with ``key``."""
+ return self._registry[key]['obj']
+
+ def __iter__(self):
+ """Returns an iterator over the keys in the registry."""
+ return self._registry.__iter__()
+
+ def __contains__(self, item):
+ return self._registry.__contains__(item)
\ No newline at end of file
--- /dev/null
+import itertools
+
+from django.template import TextNode, VariableNode, Context
+from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext, ConstantIncludeNode
+from django.utils.datastructures import SortedDict
+
+from philo.templatetags.containers import ContainerNode
+
+
+LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
+BLANK_CONTEXT = Context()
+
+
+def get_extended(self):
+ return self.get_parent(BLANK_CONTEXT)
+
+
+def get_included(self):
+ return self.template
+
+
+# We ignore the IncludeNode because it will never work in a blank context.
+setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
+setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
+
+
+def get_containers(template):
+ # Build a tree of the templates we're using, placing the root template first.
+ levels = build_extension_tree(template.nodelist)
+
+ contentlet_specs = []
+ contentreference_specs = SortedDict()
+ blocks = {}
+
+ for level in reversed(levels):
+ level.initialize()
+ contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs))
+ contentreference_specs.update(level.contentreference_specs)
+ for name, block in level.blocks.items():
+ if block.block_super:
+ blocks.setdefault(name, []).append(block)
+ else:
+ blocks[name] = [block]
+
+ for block_list in blocks.values():
+ for block in block_list:
+ block.initialize()
+ contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs))
+ contentreference_specs.update(block.contentreference_specs)
+
+ return contentlet_specs, contentreference_specs
+
+
+class LazyContainerFinder(object):
+ def __init__(self, nodes, extends=False):
+ self.nodes = nodes
+ self.initialized = False
+ self.contentlet_specs = []
+ self.contentreference_specs = SortedDict()
+ self.blocks = {}
+ self.block_super = False
+ self.extends = extends
+
+ def process(self, nodelist):
+ for node in nodelist:
+ if self.extends:
+ if isinstance(node, BlockNode):
+ self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
+ block.initialize()
+ self.blocks.update(block.blocks)
+ continue
+
+ if isinstance(node, ContainerNode):
+ if not node.references:
+ self.contentlet_specs.append(node.name)
+ else:
+ if node.name not in self.contentreference_specs.keys():
+ self.contentreference_specs[node.name] = node.references
+ continue
+
+ if isinstance(node, VariableNode):
+ if node.filter_expression.var.lookups == (u'block', u'super'):
+ self.block_super = True
+
+ if hasattr(node, 'child_nodelists'):
+ for nodelist_name in node.child_nodelists:
+ if hasattr(node, nodelist_name):
+ nodelist = getattr(node, nodelist_name)
+ self.process(nodelist)
+
+ # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
+ # node as rendering an additional template. Philo monkeypatches the attribute onto
+ # the relevant default nodes and declares it on any native nodes.
+ if hasattr(node, LOADED_TEMPLATE_ATTR):
+ loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
+ if loaded_template:
+ nodelist = loaded_template.nodelist
+ self.process(nodelist)
+
+ def initialize(self):
+ if not self.initialized:
+ self.process(self.nodes)
+ self.initialized = True
+
+
+def build_extension_tree(nodelist):
+ nodelists = []
+ extends = None
+ for node in nodelist:
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ extends = node
+ break
+
+ if extends:
+ if extends.nodelist:
+ nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
+ loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
+ nodelists.extend(build_extension_tree(loaded_template.nodelist))
+ else:
+ # Base case: root.
+ nodelists.append(LazyContainerFinder(nodelist))
+ return nodelists
\ No newline at end of file
from django.utils.html import escape, mark_safe
from django.utils.translation import ugettext_lazy as _
-from philo.utils import LOADED_TEMPLATE_ATTR
+from philo.utils.templates import LOADED_TEMPLATE_ATTR
#: Tags which are considered insecure and are therefore always disallowed by secure :class:`TemplateValidator` instances.
version = '.'.join([str(v) for v in version]),
url = "http://philocms.org/",
description = "A foundation for developing web content management systems.",
- long_description = open(os.path.join(root_dir, 'README.markdown')).read(),
+ long_description = open(os.path.join(root_dir, 'README')).read(),
maintainer = "iThink Software",
maintainer_email = "contact@ithinksw.com",
packages = packages,