Top level home page routing for CBV-based website - - how to configure the urls.py properly?

Greetings Pythonistas!

I’m writing a Django CMS for a low traffic philosophy society. It’s like a blog.

I’m building the navbar and url navigation. I’m using a DetailView CBV and a single template to handle these four basic pages:

  • Home
  • About
  • Podcasts
  • Contact

Pretty standard website layout / usecase. When a web visitor clicks on one of those list items from the navigation menu, the urls.py successfully routes the visitor to the right corresponding addresses such as:

  • http://localhost:8000/home
  • http://localhost:8000/about
  • http://localhost:8000/podcasts
  • http://localhost:8000/contact

So far so good.

My goal is to have Django serve the home page at the top level parent address http://localhost:8000/ without the “home” slug at the end (suffix). But right now, when I navigate to http://localhost:8000/, Django serves a 404 and I can’t wrap my head around how to reconfigure my two urls.py to accomplish this desired outcome.

Here is my web app’s urls.py:

from django.contrib import admin
from django.urls import path, include
from .views import PageView

app_name= 'landings'

urlpatterns = [
   path('<str:slug>/', PageView.as_view(),name='home'),
]

In the past, what I normally would do, for the first path() argument, I’d substitute an empty string like it appears here: path('', views.home, name='home').Notice the empty string as fthe first paramer. So I tried placing this line - - path('', PageView.as_view(),name='home') - - above the path() in the code snippet above but then Django throws an “AttributeError at / Generic detail view PageView must be called with either an object pk or a slug in the URLconf.” So that isn’t working in this case since I am exploring this new CBV approach to generate pages.

What do I need to change in my urls.py above (or any of my other project files below) to get Django to serve the home page at the top level address without a “/home” suffix appended to the end of the domain name?

Here is my project urls.py:

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
   path('admin/', admin.site.urls),
   path('', include('landings.urls')),
   path('articles/', include('articles.urls')),
   path('contributors/', include('contributors.urls')),
]

_navbar.html:

{% load static %}


<!-- Header Section -->
<header class="header">
   <div class="container container--narrow">
       <a href="{% url 'landings:home' 'home' %}" class="header__logo">
           <img src="Thelema_static_image_needed_here" alt="DevSearch Logo" />
       </a>


       <nav class="header__nav">
           <input type="checkbox" id="responsive-menu" />
           <label for="responsive-menu" class="toggle-menu">
               <span>Menu</span>
               <div class="toggle-menu__lines"></div>
           </label>
           <ul class="header__menu"> 
               <li class="header__menuItem"><a href="/">Home</a></li>
               <li class="header__menuItem"><a href="{% url 'landings:home' 'about' %}">About</a></li> 
               <li class="header__menuItem"><a href="{% url 'landings:home' 'podcasts' %}">Podcasts</a></li>      
               <li class="header__menuItem"><a href="{% url 'landings:home' 'contact' %}">Contact</a></li>    
           </ul>
       </nav>
   </div>
</header>

When clicking the first unordered list menu item hyperlink Home, the traceback in my Django terminal is simply:

Not Found: /
[05/Mar/2023 10:01:58] "GET / HTTP/1.1" 404 2467

The 404 showing in my browser, Django appears to be looking for the page but can’t find it and chokes at this line in my url routing: <str:slug>/ [name='home']

template:

{% extends 'base.html' %}

{% block content %}

   {% if contents %}

      {{ contents.body|safe }}

   {% endif %}

{% endblock %}

Here is my concise CBV:

from django.views.generic import DetailView
from .models import PageContent


class PageView(DetailView):
   model = PageContent
   template_name = "landings/page_detail.html"
   context_object_name = 'contents' 

My web app models.py:

from django.db import models
from ckeditor.fields import RichTextField
# from django.template.defaultfilters import slugify

class PageContent(models.Model):
   # title = models.CharField(max_length=200)
   slug = models.SlugField(null=False, unique=True, blank=True)
   page_type = models.CharField(max_length=20)
   body = RichTextField(config_name='default',max_length=300000,blank=True, null=True)

   def __str__(self):
       return f'{self.page_type}'

You can pass arguments to your view function in the url definition. See URL dispatcher | Django documentation | Django

Hi Ken. Thanks for the reply.

That doc you shared is instructive. It certainly is relevant. But I struggle with the metasyntactic placeholder variables used in their examples. They use 'foo':'bar' which I find very hard to follow and adapt to my use case.

To renew and clarify my intention (with refreshed context), here is my latest web app’s path() line:

path('the_gate', PageView.as_view(),name='the_gate'),

I’ve paired that with this corresponding line in my template:

<li class="nav-item"><a class="nav-link"  href="{% url 'stock_pages:the_gate' %}">Home   </a></li>

And when I navigate to:

http://127.0.0.1:8000/the_gate/

Django serves the page. But I am trying to get that page to load when the a web visitor navigates to:

http://127.0.0.1:8000/

That’s what I am trying to accomplish. To make it clear, that is my intention.

To that end, based on my reading of the passage in the Django doc that Ken shared, I’ve tried passing in arguments to my path() function such as:

path('', PageView.as_view(),name={"the_gate": "the_gate"}),

And:

path('', PageView.as_view(),name={"": "the_gate"}),

As well as:

path('', PageView.as_view(),name={"the_gate": ""}),

In all three cases above (and in the subsequent cases described below), I am experimenting with passing in an empty string to the url. Django returns this error:

self.view_name = ":".join(self.namespaces + [view_path])
TypeError: sequence item 0: expected str instance, dict found

The above is all wrong.

I even tried these combos too:

path('<str:slug>', PageView.as_view(),name='the_gate'),

And:

path('<str:slug>', PageView.as_view(),name={'the_gate':''}),

And:

path('<str:slug>', PageView.as_view(),name={'the_gate':'the_gate'}),

With:

<li class="nav-item"><a class="nav-link"  href="{% url 'stock_pages:the_gate' '' %}">Home   </a></li>

None of that worked. I tried additional variations of the above.

In the Django doc at the specific passage involving passing extra options to views functions (which uses the same jarring non-descriptive metasyntactic variables again) suggests that the views be called in this way with these parameters:

views.year_archive(request, year=2005, foo='bar').

There the view is function based. In my project I’m using CBVs so that adds to my bewilderment in this case.

Any idea what I could try next? Is there a way to rephrase the advice in the Django docs but with using more meaningful variable nomenclature in the context of a project with more moving parts like the one I am working on (source code below)?

I have made some changes to my source code since my original post. Here are the latest code snippets in play:

Problematic template (_navbar.html partial):

...
<li class="nav-item"><a class="nav-link"  href="{% url 'stock_pages:the_gate' %}">Home   </a></li>
           <li class="nav-item"><a class="nav-link"  href="{% url 'stock_pages:switch' 'podcasts' %}">Podcasts   </a></li>
           <li class="nav-item"><a class="nav-link"  href="{% url 'articles:article_listing' %}">Research   </a></li>
           <li class="nav-item"><a class="nav-link"  href="{% url 'contributors:writers' %}">Contributors   </a></li>
           <li class="nav-item"><a class="nav-link"  href="{% url 'stock_pages:switch' 'contact' %}">Contact</a></li>
...

Current web app urls.py:

from django.urls import path, include
from .views import PageView

app_name= 'stock_pages'

urlpatterns = [
   path('the_gate', PageView.as_view(),name='the_gate'),
   path('<str:slug>/', PageView.as_view(),name='switch'),
]

Project (parent) urls.py:

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
   path('admin/', admin.site.urls),
   path('', include('stock_pages.urls')),
   path('articles/', include('articles.urls')),
   path('contributors/', include('contributors.urls')),
]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Web app models.py:

from django.db import models
from ckeditor.fields import RichTextField
# from django.template.defaultfilters import slugify

class PageContent(models.Model):
   # title = models.CharField(max_length=200)
   slug = models.SlugField(null=False, unique=True, blank=True)
   page_type = models.CharField(max_length=20)
   body = RichTextField(config_name='default',max_length=300000,blank=True, null=True)
   pure_html_body = models.TextField(blank=True,null=True)


   def __str__(self):
       return f'{self.page_type}'

Web app views.py:

from django.views.generic import DetailView
from .models import PageContent

class PageView(DetailView):
   model = PageContent
   template_name = "landings/page_detail.html"
   context_object_name = 'contents'

Ok, from the docs:

urlpatterns = [
    path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),
]

First, notice that the parameters dict is not assigned to the name parameter, it’s a positional parameter following the url and view.

This line:

path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),

should be understood in the context of the following text:

In this example, for a request to /blog/2005/ , Django will call views.year_archive(request, year=2005, foo='bar') .

The foo in this case is the name of the parameter - it’s passed to the view in exactly the same way as year.

So in your CBV, you would access it as self.kwargs['foo'] in exactly the same way as you would access the year as self.kwargs['year'].

Or to extend this example slightly, they could also write:

path('blog/', views.year_archive, {'year': 2020, 'foo': 'bar'}),

Does this make the docs more clear?

There is no difference to the url definition whether you are using an FBV or CBV. The structure of a url does not change.

Thank you Ken for your feedback so far.

Good catch and noted.

You are right that the url structure whether FBV or CBV are the same. But the way the second path parameter in the urls.py conf is different. For example, while:

views.year_archive(year=2005, foo='bar') 

might work for FBVs, those same paramaters won’t work if they are passed in using a CBV in an urls.py such as this:

views.PageView.as_view(year=2005, foo='bar')

That won’t work.

The continued discussion involving foo and bar is still a barrier that makes me queasy.

But in terms of the logic and dictionary reference in the path() urlpattern function, I think I have some mew additional insight. I am open to being corrected. Further advice is welcome.

Here is the insight:

Usually the name argument in a path() function connects to or serves as a bridge to the string variable that is referred to in a template which in turn tells Django to serve or redirect to the given url. I believe that is accurate. So in my template below, you can see that the reference is 'the_gate'. Then when a user clicks that link, what is supposed to happen is Django will go looking for the slug, which in my case is generated by an Admin staff member who entered it in one of the content posts, and then Django will redirect to the very top level path on the site: localhost:8000/ (emphasis: no suffix or post fix). That’s what I am trying to achieve below.

Here is the relevant line in my template:

<li class="nav-item"><a class="nav-link"  href="{% url 'stock_pages:the_gate' %}">Home   </a></li>

Here is the relevant line in play in my urls.py:

path('', PageView.as_view(),{'the_gate':'the_gate'}),   

Here is the traceback:

Generic detail view PageView must be called with either an object pk or a slug in the URLconf.

That error says it needs a slug or pk. I gave it a slug in the key:value pair positional argument (not assigned to name). The key ‘the_gate’ and value ‘the_gate’ just happen to be the same strings. I tried swapping the quotes on and off with different combinations. No dice.

Also notice in this traceback it is “expecting a slug” presumably as a string inside the first path() paramater but this is precisely what I am trying to avoid: I don’t want a string to appear at the end of the top level address. So the first paramater must strictly be an empty string. Django won’t accept that.

I think you’re trying to connect things that don’t directly connect.

No, actually that would not work in the url definition of an FBV. That would be an improper definition on a couple of different levels. (Well, technically, it could be made to work, but the results aren’t what one typically expects from something like this.)

In the template at:

This is a reference to the name attribute of the url.

That name gets converted to the referenced url, which is then rendered in the html. This means that if the url is defined as:

Then that anchor tag will be rendered like this:

<a class="nav-link" href="the_gate">

Now, the issue with your url is a completely separate topic.

When you define a url as:

You have defined:

Your view is expecting a variable named slug. (Actually, the DetailView will accept either a slug or the pk by default.)

You are looking to define the value of that variable in the url definition, not the variable name itself.

Strictly speaking, the view (again, regardless of being either an FBV or CBV) does not care what the url looks like. It only cares that it receives the parameters named what it expects to receive.

I would strongly encourage you to take the time and dig through the docs and the source code of the generic CBVs to understand how they work.

I also recommend to people the Classy Class-Based Views site along with the Django Class Based Views Diagrams page.

1 Like

I’ve seen these sites before. I should have thought to have checked them out first before writing my last post. Troubleshooting on online forums can be humbling.

Based on what I learned from Classy CBVs, I experimented with building a new separate view that looked like this:

class HomeView(DetailView):
    model = PageContent
    template_name = "stock_pages/home.html"
    context_object_name = 'contents' 
    slug_field = 'slug'
    slug_url_kwarg = 'my_slug'
    
    def get_object(self, queryset=None):
        """
        Retrieve the object based on the slug instead of the pk.
        """
        slug = self.kwargs.get('slug')
        queryset = self.get_queryset()
        obj = queryset.filter(slug=slug).first()
        return obj

I then tried different variable names in my urls.py and template. After much wrangling, I resolved to writing this basic home-page specific view:

def home(request):
    contents = PageContent.objects.get(slug='home_page')
    context = {'contents': contents}
    return render(request, 'stock_pages/home.html', context=context)

This is my latest path function:

path ('', views.home, name='home_page'),

This works.

In the end I didn’t even have to write a separate model. I just used the existing one so my Admin Dashboard looks clean just as it did before.

I realize this is not best practices because the home view above uses a get() query meaning that it is hard coded and not dynamic. Alas, I digress.

Yep, sure does!

Now, to wrap this back up, you’ve got:

combined with:

Would give you: