Building published/unpublished feature for blog CMS

Hello Pythonistas!

I’m trying to write a basic “is_published” switch feature for posting content for the rudimentary Django CMS blog I am building. I’m trying to enable a user to flip the switch in the Admin Dashboard to turn a blog post from being accessible or inaccessible on the website. I’m using CBVs. My best attempt at crafing my DetailView can be found below, along with the template and the relevant snippet from my model.

In terms of behavior, currently whether the is_published switch is turned on or off in the Admin Dashboard, Django is serving the template, but the title, author, or body data point content are empty. It’s just a blank page in both cases. What I am trying to do is have Django serve a 404 when is_published is turned False and have Django serve the template and data text content when trned True. What is interesting is that when I comment out the get_context_data class method override, Django serves the text data successfully.

Any idea what I am doing wrong?

To come with the below, I leveraged this specific passage from the Django Docs on DetailViews: Generic display views | Django documentation | Django

What might you people recommend I try next?

class PreambleDetailView(LoginRequiredMixin,DetailView):
    model = Preamble
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        try:
            context['preambles'] = Preamble.objects.filter(is_published=True)
        except Preamble.DoesNotExist:
            raise Http404('Article does not exist!')
        return context
    
    context_object_name = 'preambles'

Here is my template:

{% extends 'base.html' %}

{% block content %}

<div class="body">
        
    {% if preambles  %}
        <h1 class="title">{{preambles.title}}</h1>
        <!-- <h3><em>Preamble</em></h3> -->
        <div class="author">Author: {{preambles.author}}</div>
        <div class="detail">{{preambles.body|safe}}</div> <!-- end detail class -->
    {% endif %}

</div> <!-- end body -->

{% endblock %}

Here is my models.py:

from django.db import models

class GatewayProtect(models.Model):
    is_protected = models.BooleanField(default=True)
    
class Content(models.Model):
    pass

class Preamble(models.Model):
    title = models.CharField(max_length=300,blank=True)
    is_published = models.BooleanField(default=True)
    author = models.CharField(max_length=30,blank=True)
    slug = models.SlugField(unique=True,blank=True)
    body = models.TextField(max_length=300000,blank=True)

A DetailView accepts the pk parameter to select a single instance of the referenced model (Preamble). You then have access to that object as self.object and self.preamble.

In your get_context_data, you’re going to retrieve all Preamble objects where is_published == True. It does not apply this to the instance of the object automatically retrieved. Also, you’re overriding your class definition of context_object_name by reusing preambles in this method, which is preventing the template from working.

Side note: That filter will never throw a DoesNotExist exception. Filters will return empty querysets.

What you more likely want to override is get_object. You can call super in your get_object method to get the object, then check the is_published attribute to determine whether you want to display that object.

And get rid of the get_context_data method.

I partially understand what you are saying. My intention is to retrieve (show) only the templates which have the is_published trigger enabled in the Admin Dashboard. But the way I had been using get_context_data method in my last post, it was retrieving all of them that were True. I just want to disable access to templates that have is_published un-ticked.

I have since removed the context_object_name = 'preambles' from this view. But then how does my web app know to return 'preambles' to be referenced in my template?

I understand that this is a side note but I have a further point of clarification here. According to the official Django docs for QuerySets, it explains filter() right out of the gate near the top. It is sparse and doesn’t say anything about empty querysets. If they will always return empty querysets, then how else can they be used effectively? Alternatively, there are methods that return querysets like get() and create(). As you can see in my latest attempt writing my CBV below, I make use of .get() quite a bit. Other alternatives in the official QuerySet doc explain that a host of Field Lookups can be used such as contains() or exact() but in my use case, I need something to lookup a model attribute is a Bool. Which of these querysets do I need for this use case? Is get() the right one?

Noted. To this end I have leveraged the Classy CBV website for DetailViews, specifically the get_object() method section. Since I am still relatively new to CBV’s, I progressively tried entering certain lines, and snippets from that doc which always broke my Django shell. It got to the point where I just used the whole snippet for the get_object() method and Django finally accepted that. I know that kind of defeats the purpose because when using DetailView, it comes with the get_object() function by default. What I am trying to do is over-ride it to add custom functionality. So haven’t reached my original goal because now the template is being served but the content is blank whether I have the is_published model attribute switched on or off. So I am still not getting it.

@KenWhitesell suggested that I need to use the super() method somewhere in here. I couldn’t find anything in the official Django docs specific to super(). It’s used in passing on Classy CBVs without much explanatory detail. With a Google search I found some outdated Stack Overflow questions and answers which I am skeptical of since the Django code base has changed so much over the past 12 years. I learned my lesson to not take SO very seriously when it comes to using it as a resource for building modern Django projects.

Below is my latest DetailView CBV for this web app. As you’ll notice, it’s almost a verbatim snippet from ccbv.co.uk. That’s the only way Django wouldn’t break. I still need some guidance on how to modify this so that Django serves a 404 instead of the template when the is_published attribute (Bool) is switched to off.

class PreambleDetailView(LoginRequiredMixin,DetailView):
    model = Preamble
     
    def get_object(self, queryset=None):
        if queryset is None:
            queryset = self.get_queryset()
        pk = self.kwargs.get(self.pk_url_kwarg)
        slug = self.kwargs.get(self.slug_url_kwarg)   
        if slug is not None and (pk is None or self.query_pk_and_slug):
            slug_field = self.get_slug_field()
            queryset = queryset.filter(**{slug_field: slug})
        if pk is None and slug is None:
            raise AttributeError(
            "Generic detail view %s must be called with either an object "
            "pk or a slug in the URLconf." % self.__class__.__name__
            )
        try:
            # Get the single item from the filtered queryset
            obj = queryset.get()
        except queryset.model.DoesNotExist:
            raise Http404(
                _("No %(verbose_name)s found matching the query")
                % {"verbose_name": queryset.model._meta.verbose_name}
            )
        return obj
    
    # context_object_name = 'preambles'

They don’t always return empty querysets. It’s just that it will return an empty queryset when no rows matching that filter are found.

Or to phrase it a different way, if a filter doesn’t result in any rows matching that filter, it will return an empty queryset instead of throwing an error.

The basic conceptual differences between filter and get:

  • Filter returns a queryset that can then be modified by chaining other functions. Get returns one instance of an object.
  • Filter can return an empty queryset if the filters result in no rows being returned. Get thrown an error in that situation
  • Filter can return multiple rows in the queryset if the filter results in multiple rows being returned. Get throws an error in that situation.

Specifically, this means that this statement:

is fundamentally incorrect. (Neither get nor create returns a queryset.)

The QuerySet API specifically enumerates which functions return a queryset and which don’t.

This is an extremely important point to be aware of, because while you can chain functions that each return a queryset (for example, adding an exclude function after a filter), using a function that doesn’t return a queryset stops any further function chains from being applied.

MyModel.objects.filter(bool_attribute=True)
or, if you already have an instance of the object that you want to check:
if my_model.bool_attribute
(It is already a boolean value - you don’t need to specify if my_model.bool_attribute = True in your condition.)

The super function is a standard Python function, it has nothing specifically to do with Django.

If you’re not comfortable with it and what it means, you probably want to spend some time learning about Python classes.

If you’re going to use the CBVs, you also need to be very comfortable with class inheritance, mixins, and the MRO.

Because you are directly adding it to the context using this line:

Fundamentally, the functional basis of the Django-provided detail CBVs is that:

  • They’re designed to work with one instance of one object
  • The model to be used as that object is identified by the model attribute
  • The get_object function is intended to retrieve one instance of that identified model.
  • The get_context_data is intended to retrieve any additional data to be used by that view when rendering the template.

Side note: My opinions on SO are well-known in this forum and in other circles. I’m not going to repeat them here.

So this is one final test to be added to this method.

Right near the bottom you have:

You can add an if statement immediately before the return statement. You can check obj.is_published for the value you don’t want, and if it’s the value you don’t want, then raise the Http404 exception in the same manner as the code in the except block above it.

This brings us back to the super issue. In this case, if you create your own get_object method, a call to super is going to call the parent’s class get_object function, returning obj to you. So all you really need to write for your get_object method is a call to super (saving the return value), your test on that value with the appropriate raise, and returning that same value.

1 Like

This is very clear. I tried a few different variations before finally settling on this:

        try:
            # Get the single item from the filtered queryset
            obj = queryset.get()
        except queryset.model.DoesNotExist:
            raise Http404(
                _("No %(verbose_name)s found matching the query")
                % {"verbose_name": queryset.model._meta.verbose_name}
            )
        if obj.is_published==True:
            return obj
        else:
            raise Http404('Page Redacted')

Eureka! It works!

You recommended checking if obj.is_published is False, but checked if it were True and then reversed the positioning of the two conditionals’ consequents.

I found a primer by the Real Python folks titled: “Supercharge Your Classes With Python super()”. I read the Overview section and it makes sense to me generally. I haven’t read the entire guide but I will.

I experimented with using the super() function trying to join a few different operations with variables and various arguments but I couldn’t get it to work. I’ll leave it for now. After reading Real Python’s tutorial linked to above, I may return to this problem at a later date.

Thanks @KenWhitesell for your patience once again seeing me through my Django exploratory novice questions and spending so much time explaining and guiding me until I reach a final solution. :slight_smile:

One final thought - you clarified the difference between filter() and get():

To add to your point (to see if I understand this), when a filter expression is called in a view and returned in a context dictionary variable, the template will print nothing. For example, a Jinja for loop list will print nothing when filter() returns an empty query set. Whereas when a get() expression in a view returns nothing, Django won’t even serve the template, instead Django will serve a 500 (or a 404?) because get() must return a single object as output. Is this accurate? Please correct me.

Side note: this code

can also be written as:

        if obj.is_published:
            return obj
        else:
            raise Http404('Page Redacted')

or

        if not obj.is_published:
            raise Http404('Page Redacted')
        return obj

Continuing on -

This is true if the template is iterating over the queryset and doesn’t check for an empty queryset. (Which may or may not be what you want, depending upon your requirements for that template.)

This is correct. (500, unless you use the get_object_or_404 convenience function)