Skip to content

Commit

Permalink
Merge e356b97 into 18bbb24
Browse files Browse the repository at this point in the history
  • Loading branch information
ninepints committed May 13, 2018
2 parents 18bbb24 + e356b97 commit 8f212d2
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 12 deletions.
58 changes: 51 additions & 7 deletions bakery/feeds.py
@@ -1,4 +1,5 @@
import os
import six
import logging
from django.conf import settings
from bakery.views import BuildableMixin
Expand All @@ -12,16 +13,59 @@ class BuildableFeed(Feed, BuildableMixin):
"""
build_path = 'feed.xml'

def get_content(self):
return self(self.request).content
def get_content(self, *args, **kwargs):
return self(self.request, *args, **kwargs).content

@property
def build_method(self):
return self.build_queryset

def _get_bakery_dynamic_attr(self, attname, obj, args=None, default=None):
"""
Allows subclasses to provide an attribute (say, 'foo') in three
different ways: As a fixed class-level property or as a method
foo(self) or foo(self, obj). The second argument argument 'obj' is
the "subject" of the current Feed invocation. See the Django Feed
documentation for details.
This method was shamelessly stolen from the Feed class and extended
with the ability to pass additional arguments to subclass methods.
"""
try:
attr = getattr(self, attname)
except AttributeError:
return default

if callable(attr) or args:
args = args[:] if args else []

# Check 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.
try:
code = six.get_function_code(attr)
except AttributeError:
code = six.get_function_code(attr.__call__)
if code.co_argcount == 2 + len(args): # one argument is 'self'
args.append(obj)
return attr(*args)

return attr

def get_queryset(self):
return [None]

def build_queryset(self):
logger.debug("Building %s" % self.build_path)
self.request = self.create_request(self.build_path)
self.prep_directory(self.build_path)
path = os.path.join(settings.BUILD_DIR, self.build_path)
self.build_file(path, self.get_content())
for obj in self.get_queryset():
build_path = self._get_bakery_dynamic_attr('build_path', obj)
url = self._get_bakery_dynamic_attr('feed_url', obj)

logger.debug("Building %s" % build_path)

self.request = self._get_bakery_dynamic_attr(
'create_request', obj, args=[url or build_path])

self.prep_directory(build_path)
path = os.path.join(settings.BUILD_DIR, build_path)
content = self._get_bakery_dynamic_attr('get_content', obj)
self.build_file(path, content)
31 changes: 29 additions & 2 deletions bakery/tests/__init__.py
Expand Up @@ -96,6 +96,26 @@ def items(self):
return MockObject.objects.all()


class MockSubjectRSSFeed(feeds.BuildableFeed):
link = '/latest.xml'

def get_object(self, request, obj_id):
return MockObject.objects.get(pk=obj_id)

def get_queryset(self):
return MockObject.objects.all()

def get_content(self, obj):
return super(MockSubjectRSSFeed, self).get_content(obj.id)

def build_path(self, obj):
return str(obj.id) + '/feed.xml'

def items(self, obj):
# Realistically there would be a second model here
return MockObject.objects.none()


class JSONResponseMixin(object):

def render_to_response(self, context, **response_kwargs):
Expand Down Expand Up @@ -290,12 +310,19 @@ def test_json_view(self):

def test_rss_feed(self):
f = MockRSSFeed()
f.build_method
f.build_queryset()
f.build_method()
build_path = os.path.join(settings.BUILD_DIR, 'feed.xml')
self.assertTrue(os.path.exists(build_path))
os.remove(build_path)

def test_subject_rss_feed(self):
f = MockSubjectRSSFeed()
f.build_method()
for obj in MockObject.objects.all():
build_path = os.path.join(settings.BUILD_DIR, str(obj.id), 'feed.xml')
self.assertTrue(os.path.exists(build_path))
os.remove(build_path)

def test_build_cmd(self):
call_command("build", **{'skip_media': True, 'verbosity': 3})
call_command("build", **{'skip_static': True, 'verbosity': 3})
Expand Down
40 changes: 37 additions & 3 deletions docs/buildablefeeds.rst
Expand Up @@ -20,23 +20,57 @@ BuildableFeed

.. py:attribute:: build_method
An alias to the ``build_queryset`` method used by the :doc:`management commands </managementcommands>`
An alias to the ``build_queryset`` method used by the :doc:`management commands </managementcommands>`.

.. py:method:: build_queryset()
Writes the rendered template's HTML to a flat file. Only override this if you know what you're doing.

.. py:method:: get_queryset()
The ``Feed`` class allows a single feed instance to return different content for requests to different URLs.
The "subject" for a request is determinted by the object returned from the ``get_object`` method, by default ``None``.
(See `the Django docs <https://docs.djangoproject.com/en/dev/ref/contrib/syndication/#a-complex-example>` for details.)
Override this method to provide a collection of "subjects" for which bakery should render the feed.

As in Django, you can replace certain bakery feed attributes (such as ``build_path``) with methods that accept the subject as an extra "obj" parameter.

**Example myapp/feeds.py**

.. code-block:: python
from myapp.models import MyModel
import os
from myapp.models import MyModel, MyParentModel
from bakery.feeds import BuildableFeed
class ExampleRSSFeed(BuildableFeed):
link = 'http://www.mysite.com/rss.xml'
link = '/'
feed_url = '/rss.xml'
build_path = 'rss.xml'
def items(self):
return MyModel.objects.filter(is_published=True)
class ExampleFeedWithSubject(BuildableFeed):
def get_object(self, request, obj_id):
return MyParentModel.objects.get(pk=obj_id)
def get_queryset(self):
return MyParentModel.objects.filter(is_published=True)
def get_content(self, obj):
return super().get_content(obj.id)
def link(self, obj):
return obj.get_absolute_url()
def feed_url(self, obj):
return os.path.join(obj.get_absolute_url(), 'rss.xml')
def build_path(self, obj):
return self.feed_url(obj)[1:] # Discard initial slash
def items(self, obj):
return MyModel.objects.filter(parent__id=obj.id)

0 comments on commit 8f212d2

Please sign in to comment.