URL of CreateView doesn't work

Django 5, Python 3.12,
I’m developing a forum app, its structure roughly is “forum → subforum → topic → comment”. The number of subforum limited by “choices”, inside every subforum only related topics are seen and available, also there was implemented a filter system for topics (filtering inside every subforum separately).

Now it’s time to implement a creation of topics function, so I’ve made corresponding view (AddTopic), template (addtopic.html) and URL (“add_topic/”). I’ve decided to put the link to “add_topic/” to subforum template ("subforum.html), where the list of related topics is displayed, right under the list. But when I run test server and try to click on the “Create new topic” button, it displays an error (production - is one of subforums, available via slug):

Page not found (404)
No Topic found matching the query
Request Method:	GET
Request URL:	http://127.0.0.1:8000/forum/production/add_topic/
Raised by:	forum.views.ShowTopic
Using the URLconf defined in django_project.urls, Django tried these URL patterns, in this order:

admin/
[name='home']
about [name='about']
characters/ [name='characters']
episodes/ [name='episodes']
posts/<slug:post_slug> [name='posts']
characters/<slug:char_slug> [name='chars']
news/
users/
user_page/
forum/ [name='forum']
forum/ <slug:subforum_slug>/ [name='subforum']
forum/ <slug:subforum_slug>/<slug:topic_slug>/ [name='topic']
The current path, forum/production/add_topic/, matched the last one.

I thought I did all according to the tutorial, so I’m struggling to find what I have done wrong. So I ask the community’s help and advice.

views.py:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django_filters.views import FilterView

from core.views import menu
from .filters import TopicFilter
from .forms import AddTopicForm, AddCommentForm
from .models import Subforum, Topic, Comment, Profile
from .utils import DataMixin


class SubForumListView(ListView):
    model = Subforum
    context_object_name = 'subforum_list'
    template_name = "forum/forum.html"

    def get_context_data(self, **kwargs):
        subforums = Subforum.objects.all()
        context = {'subforums': subforums}
        return context


class TopicListView(FilterView):
    model = Topic
    template_name = "forum/subforum.html"
    slug_url_kwarg = 'subforum_slug'
    context_object_name = 'topics'
    filterset_class = TopicFilter

    def get_queryset(self):
        qs = self.model.objects.all()
        if self.kwargs.get('subforum_slug'):
            qs = qs.filter(subforum__slug=self.kwargs['subforum_slug'])
        return qs


class ShowTopic(DetailView):
    model = Topic
    template_name = "forum/topic.html"
    slug_url_kwarg = 'topic_slug'
    context_object_name = 'topic'

    def get_context_data(self, **kwargs):
        topic = get_object_or_404(Topic, slug=self.kwargs['topic_slug'])
        comments = Comment.objects.filter(topic=topic)
        comments_number = len(Comment.objects.filter(topic=topic))
        context = {'menu': menu,
                   'topic': topic,
                   'comments': comments,
                   'comm_num': comments_number}
        return context


class AddTopic(DataMixin, CreateView):
    form_class = AddTopicForm
    template_name = 'forum/addtopic.html'
    page_title = 'Create a new topic'


class AddComment(LoginRequiredMixin, DataMixin, CreateView):
    form_class = AddCommentForm
    template_name = 'forum/addcomment.html'
    page_title = 'Оставить комментарий'
    success_url = reverse_lazy('topic')


class UpdateComment(LoginRequiredMixin, DataMixin, UpdateView):
    form_class = AddCommentForm
    template_name = 'forum/addcomment.html'
    page_title = 'Редактировать комментарий'
    success_url = reverse_lazy('topic')

subforum.html:

{% extends 'base.html' %}

{% block content %}
<h1>{{ subforum.title }}</h1>

    {% block sidebar %}
    <div class="container">
        <div class="container-posts">
            <form method="get">
                {% csrf_token %}
                {{ filter.form.as_p }}
                <button type="submit">Search</button>
            </form>
        </div>
    </div>
    {% endblock %}

<div class="container-posts">
    <h3>Обсуждения</h3>
    <div class="row">
        {% for t in topics %}
        <div class="card-body">
            <a href="{{ t.get_absolute_url }}">{{ t.subject }}</a>
        </div>
        {% endfor %}
    </div>
</div>

<div class="container-posts">
    <a href="add_topic"><button>Create a new topic</button></a>
</div>
{% endblock %}

addtopic.html:

{% extends 'base.html' %}

{% block content %}
<h1>New topic</h1>
<form action="" method="post">
    {% csrf token %}
    <div class="form-error">{{ form.non_field_errors }}</div>
    {% for f in form %}
        <p><label class="form-label" for="{{ f.id_for_label }}">{{ f.label }}</label>{{ f }}</p>
        <div class="form-error">{{ f.errors }}</div>
    {% endfor %}
    <p><button type="submit">Create topic</button></p>
</form>
{% endblock %}

models.py:

from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.text import slugify

from .consts import *


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    surname = models.CharField(max_length=32, default='')
    name = models.CharField(max_length=32, default='')
    email = models.EmailField(max_length=254, blank=True, unique=True)
    bio = models.TextField(max_length=500, default="Write a couple of words about yourself")
    avatar = models.ImageField(default=None, blank=True, max_length=255)
    status = models.CharField(max_length=25, blank=True, default='')
    slug = models.SlugField()
    age = models.IntegerField(verbose_name='Возраст', null=True, blank=True)
    gender = models.CharField(verbose_name='Пол', max_length=32, choices=Genders.GENDER_CHOICES, default="H", blank=True)
    reputation = models.IntegerField(verbose_name='Репутация', default=0)

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

    def get_absolute_url(self):
        return reverse('forum:user_profile', kwargs={'profile_slug': self.slug})

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = slugify(self.user.username)
            return super(Profile, self).save(*args, **kwargs)


class Subforum(models.Model):
    title = models.CharField(verbose_name='Название', max_length=32, choices=Theme.THEME_CHOICES, default=1)
    slug = models.SlugField(default='News')
    objects = models.Manager()

    class Meta:
        ordering = ['title']
        verbose_name = 'Разделы форума'
        verbose_name_plural = 'Разделы форума'

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = slugify(self.title)
            return super(Subforum, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('forum:subforum', kwargs={'subforum_slug': self.slug})


class Topic(models.Model):
    subject = models.CharField(verbose_name='Заголовок', max_length=255, unique=True)
    first_comment = models.TextField(verbose_name='Сообщение', max_length=2000, default='')
    slug = models.SlugField(default='', unique=True, max_length=25, editable=False)
    subforum = models.ForeignKey('Subforum',
                                 verbose_name='Раздел',
                                 on_delete=models.CASCADE,
                                 related_name='subforum')
    creator = models.ForeignKey(User,
                                verbose_name='Создатель темы',
                                on_delete=models.SET('deleted'),
                                related_name='creator')
    created = models.DateTimeField(auto_now_add=True)
    closed = models.BooleanField(default=False)
    objects = models.Manager()

    class Meta:
        ordering = ['id']
        verbose_name = 'Обсуждения'
        verbose_name_plural = 'Обсуждения'

    def __str__(self):
        return self.subject

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = f'topic-{slugify(self.subject)}'
            return super(Topic, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('forum:topic', kwargs={'topic_slug': self.slug, 'subforum_slug': self.subforum.slug})


class Comment(models.Model):
    topic = models.ForeignKey('Topic',
                              verbose_name='Тема',
                              on_delete=models.CASCADE,
                              related_name='comments')
    author = models.ForeignKey(User,
                               verbose_name='Комментатор',
                               on_delete=models.SET('deleted'),
                               related_name='author')
    content = models.TextField(verbose_name='Текст', max_length=2000)
    created = models.DateTimeField(verbose_name='Дата публикации', auto_now_add=True)
    updated = models.DateTimeField(verbose_name='Дата изменения', auto_now=True)
    objects = models.Manager()

    class Meta:
        ordering = ['created']
        verbose_name = 'Комментарии'
        verbose_name_plural = 'Комментарии'

    def __str__(self):
        return f'Post of {self.topic.subject} is posted by {self.author.username}.'

forum/urls.py:

from django.urls import path

from forum.views import *


app_name = 'forum'

urlpatterns = [
    path('', SubForumListView.as_view(), name='forum'),
    path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
    path('<slug:subforum_slug>/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
    path('<slug:subforum_slug>/add_topic/', AddTopic.as_view(), name="add_topic"),
    path('<slug:subforum_slug>/<slug:topic_slug>/add-comment/', AddComment.as_view(), name="add_comment"),
    path('<slug:subforum_slug>/<slug:topic_slug>/edit/<int:id>/', UpdateComment.as_view(), name="edit_comment"),
]

project/urls.py:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('core.urls')),
    path('news/', include('core.urls')),
    path('users/', include('users.urls', namespace="users")),
    path('user_page/', include('users.urls')),
    path('forum/', include('forum.urls', namespace="forum")),
]

If any additional info is required, I’m ready to provide it.

The issue here is that URLs are searched sequentially. The first pattern that a URL matches will be the one that is used.

Since the literal string “add_topic” would be a valid slug, the url http://127.0.0.1:8000/forum/production/add_topic/ is going to match the first pattern, and end up calling the view ShowTopic as shown in your error message.

1 Like

Mr. KenWhitesell,
Aha, so, your suggestion is to push “add_topic” URL upper, did I understand you right?
Strange case enough, as I have no “add_topic” slug among topics.

That is one work-around, but not one I would recommend. If you make that change, then if you do end up with a slug add_topic, then you would never be able to show it.

My recommendation would be to better qualify your URLs. Something more like:
path('<slug:subforum_slug>/show/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),

This really isn’t a “strange” or “unusual” situation. The pattern being supplied to the view isn’t validated at the time the URL is matched - it’s done in the view itself. See the docs at URL dispatcher | Django documentation | Django for more details about this.

1 Like

Mr. KenWhitesell,
Thank you very much for your insight!
A good point indeed. Although, does it count as a good practise to make URLs that complicated? I’m not arguing that, just interested (I’m not really experienced in that area).

I wouldn’t call that complicated, it’s not as lengthy or as complex as the examples in the docs I linked to.

The “good practice” here is to ensure that your URLs are distinct, such that a requested pattern is dispatched to the correct view. Trying to shorten URLs is a bad practice if it leads to the situation where a requested url matches more than one pattern, causing ambiguity or causing the wrong view to be executed.

1 Like

Thank you once more, it was an important knowledge!