Django: Things you don't learn from the tutorials
5 min read
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
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
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.
This means you're passing a class to the path function. While this:-
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), # ... ]
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
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.