Let's say you want to add RSS feed to your blog built using Django. Django has syndication feed framework so that's probably the one that you'll use.
I'll just take the code example from the documentation linked above. Your feed's code will look like this:-
from django.contrib.syndication.views import Feed
from django.urls import reverse
from policebeat.models import NewsItem
class LatestEntriesFeed(Feed):
title = "Police beat site news"
link = "/sitenews/"
description = "Updates on changes and additions to police beat central."
def items(self):
return NewsItem.objects.order_by('-pub_date')[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return item.description
# item_link is only needed if NewsItem has no get_absolute_url method.
def item_link(self, item):
return reverse('news-item', args=[item.pk])
And will hook that feed class into urls.py
like this:-
from django.urls import path
from myproject.feeds import LatestEntriesFeed
urlpatterns = [
# ...
path('latest/feed/', LatestEntriesFeed()),
# ...
]
All is well and good until there's a new requirement that the feeds listed should be based on the user's current language. So how do we know what is the user's current language?
User's language can be inferred from the Request
object as request.LANGUAGE_CODE
. But no where in code above we can see a request
object.
Usually, we will see the request object when we implement the views function, such as in the code below:-
from django.http import HttpResponse
def show_feed(request):
return HttpResponse("Hello world")
I'm just too lazy to show the example above as CBV.
But our feed's code above looks nothing like views, be it a CBV or FBV. Depending on your knowledge and understanding of Django, you might see a hint that the above code is also a views or you will have no idea at all.
The most obvious hint would be the code in the urls.py
where we hook up our Feed class to a url path. Let's see it again:-
urlpatterns = [
# ...
path('latest/feed/', LatestEntriesFeed()), # ...
]
The second parameter to the path()
function must be a views (or a urls include but let's save that for the other day). So LatestEntriesFeed()
will act as a views. How can that be? We defined it as a class of Feed
.
Some might be confused that we're passing class LatestEntriesFeed
to the path()
function. This is a common mistake that I observed among beginners or junior developers. The parentheses at the end make a difference.
path('latest/feed/', LatestEntriesFeed),
This means you're passing a class to the path function. While this:-
path('latest/feed/', LatestEntriesFeed()),
You're passing an instance of that class. Writing it like the below maybe will make it much clearer:-
feed_views = LatestEntriesFeed()
urlpatterns = [
# ...
path('latest/feed/', feed_views), # ...
]
NOTES:-
While reading the source code for django syndication framework, I noticed the use of get_language()
function from django.utils.translation
. So the rest of this article is just purely an academic exercise ... :(
There are 2 ways we can figure out where the request
object exists. First is by using breakpoint()
to step up the caller of our method and see at which point request
object exists. The second is by reading the source code.
Let us take a look at the first method. Put a breakpoint in our items()
method.
class LatestEntriesFeed(Feed):
title = "Police beat site news"
link = "/sitenews/"
description = "Updates on changes and additions to police beat central."
def items(self):
breakpoint()
return NewsItem.objects.order_by('-pub_date')[:5]
When we make a request again, we should get something like this on our console:-
> /workspace/kai-site/src/kai_site/feeds.py(14)items()
-> return BlogPage.objects.live().order_by('-date')
(Pdb) l
9
12 def items(self, request):
13 breakpoint()
14 -> return BlogPage.objects.live().order_by('-date')
15
16 def item_title(self, item):
17 return item.title
18
19 def item_description(self, item):
Step one level up will give us this:-
(Pdb) u
> /workspace/.pyenv_mirror/poetry/virtualenvs/kai-site-gqAITAgE-py3.10/lib/python3.10/site-packages/django/contrib/syndication/views.py(103)_get_dynamic_attr()
-> return attr(obj)
Step up again and we will see this:-
-> for item in self._get_dynamic_attr("items", obj):
(Pdb) l
175 try:
176 description_tmp = loader.get_template(self.description_template)
177 except TemplateDoesNotExist:
178 pass
179
180 -> for item in self._get_dynamic_attr("items", obj):
181 context = self.get_context_data(
182 item=item, site=current_site, obj=obj, request=request
183 )
184 if title_tmp is not None:
185 title = title_tmp.render(context, request)
That's it! We can see the request
object. So our Feed
class does has the request
object somewhere but it's not available to our method.
If the Feed class is implemented as CBV, then our instance's self
will have the request
object. This syndication framework I guess was implemented before the CBV implementation. The pattern used here is one of the proposed CBV implementation but as history told us, the class method approach is finally the one that gets implemented.
Btw, let's get back to the question of how our feed class can act like a Views. If you look into the source code of the syndication framework, we can see this line of code:-
class Feed:
feed_type = feedgenerator.DefaultFeed
title_template = None
description_template = None
language = None
def __call__(self, request, *args, **kwargs):
try:
obj = self.get_object(request, *args, **kwargs)
except ObjectDoesNotExist:
raise Http404("Feed object does not exist.")
feedgen = self.get_feed(obj, request)
response = HttpResponse(content_type=feedgen.content_type)
if hasattr(self, "item_pubdate") or hasattr(self, "item_updateddate"):
# if item_pubdate or item_updateddate is defined for the feed, set
# header so as ConditionalGetMiddleware is able to send 304 NOT MODIFIED
response.headers["Last-Modified"] = http_date(
feedgen.latest_post_date().timestamp()
)
feedgen.write(response, "utf-8")
return response
The class implemented the magic method __call__
which will turn an instance of that class into a callable. In Python, a callable is an object that we can call (doh ...). The most common callable is a function. A class is also callable. Calling a class will return an instance of that class.
Now let's get into the actual question we have - how do we get the current language from our items()
method? By default, the items()
method get called without any parameter. But if we add a second parameter to the method, it will pass any object returned by the get_object()
method. The default implementation in the Feed class only returns None so we need to override the method.
class LatestEntriesFeed(Feed):
title = "Police beat site news"
link = "/sitenews/"
description = "Updates on changes and additions to police beat central."
def items(self, request):
language = request.LANGUAGE_CODE
return (NewsItem.objects.filter(language=language)
.order_by('-pub_date')[:5])
def item_title(self, item):
return item.title
def get_object(self, request, *args, **kwargs):
return request
As we can see above, get_object()
get passed a request
object. The actual purpose of this method is to return our Feed object but since we don't have any, we just return the request
object. The object then will get passed to items()
the method as second parameter. If we do have a feed object to return, then can attach the request object to that object instead.