Django: Things you don't learn from the tutorials

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.