Django views' tests return 403/200 code instead of (presumably) 302

I’m testing views in my Django app. As the app is a backend of a forum site, I’m trying to test the creation, editing and deletion of a topic.

Creation, editing and deletion of a topic are implemented in my app to work via redirect:

  • creation page (AddTopic) redirects to a succefully created topic’s page;
  • editing the topic’s initial comment (UpdateFirstComment) redirects from the editing page to the edited topic’s page;
  • deletion page (DeleteTopic) redirects to a subforum (a chapter of a forum) where the deleted topic had belonged.

I presume (I’m not sure; and, most possibly, here is my mistake) that the successful redirect code is 302, and in the tests’ assertion that’s the code which should be checked.
But in practice, creation and editing tests return code 200, while the deletion test returns code 403. And I, due to the lack of experience, hardly can explain why it happens this way and how to deal with it.

views.py:

class TopicListView(FilterView):
    paginate_by = 20
    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'
    paginate_by = 5

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        topic = self.get_object()

        comments_list = Comment.objects.filter(topic=topic).order_by('created')
        paginator = Paginator(comments_list, self.paginate_by)
        page_number = self.request.GET.get('page', 1)
        page_obj = paginator.get_page(page_number)

        context.update({
            'menu': menu,
            'comments': page_obj,
            'page_obj': page_obj,
            'is_paginated': page_obj.has_other_pages(),
            'paginator': paginator,
            'comm_num': comments_list.count(),
            #'topic_rep': topic.total_rep,
        })

        return context


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

    def get_success_url(self):
        return reverse('forum:topic', kwargs={
            'subforum_slug': self.kwargs['subforum_slug'], 'topic_slug': self.object.slug})

    def form_valid(self, form):
        subforum = Subforum.objects.get(slug=self.kwargs['subforum_slug'])
        form.instance.creator = self.request.user
        form.instance.subforum = subforum
        return super(AddTopic, self).form_valid(form)


class UpdateFirstComment(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Topic
    form_class = AddTopicForm
    template_name = 'forum/editcomment.html'
    page_title = 'Edit topic'

    def test_func(self):
        topic = self.get_object()
        if self.request.user == topic.creator or self.request.user.is_superuser:
            return True
        return False

    def get_success_url(self):
        return reverse('forum:topic', kwargs={
            'subforum_slug': self.kwargs['subforum_slug'],
            'topic_slug': self.kwargs['topic_slug']
        })

    def get_object(self, queryset=None):
        return Topic.objects.get(slug=self.kwargs['topic_slug'], subforum__slug=self.kwargs['subforum_slug'])

    def form_valid(self, form):
        self.object = form.save(commit=False)
        first_comment = self.object.first_comment
        form.instance.creator = self.request.user
        form.instance.topic = self.object
        form.instance.first_comment = first_comment
        return super(UpdateFirstComment, self).form_valid(form)


class DeleteTopic(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Topic
    context_object_name = 'topic'
    template_name = 'forum/topic_confirm_delete.html'
    page_title = "Delete topic"
    fields = '__all__'

    def test_func(self):
        if self.request.user.is_superuser:
            return True
        return False

    def get_success_url(self):
        return reverse('forum:subforum', kwargs={'subforum_slug': self.kwargs['subforum_slug']})

forum/urls.py (only for subforums and topics):

<...>
app_name = 'forum'

urlpatterns = [
    path('', SubForumListView.as_view(), name='forum'),
    path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
    path('<slug:subforum_slug>/add_topic/', AddTopic.as_view(), name="add_topic"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/edit_topic/', UpdateFirstComment.as_view(), name='edit_topic'),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/delete_topic/', DeleteTopic.as_view(), name='delete_topic'),
]

tests.py (comments’ section tests are omitted, they work fine):

from django.test import TestCase
from django.urls import reverse

from . import factories, models
from .models import Topic, Comment


class SubforumTestCase(TestCase):
    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.user = factories.UserFactory()
        self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
        self.client.force_login(self.user)

    def test_get_topic_list(self):
        url = reverse('forum:subforum', kwargs={'subforum_slug': self.subforum.slug})
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['topics'].count(), models.Topic.objects.count())

    def test_get_topic_detail(self):
        url = reverse("forum:topic", kwargs={'subforum_slug': self.subforum.slug, 'topic_slug': self.topic.slug})
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, "forum/topic.html")

    def test_add_topic(self):
        data = {
            'subject': self.topic.subject,
            'first_comment': self.topic.first_comment
        }
        url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
        old_topics_count = Topic.objects.count()
        response = self.client.post(url, data=data)

        self.assertEqual(response.status_code, 302)
        self.assertEqual(Topic.objects.count(), 2)
        self.assertGreater(Topic.objects.count(), old_topics_count)

    def test_update_first_comment(self):
        data = {
            'first_comment': "Chebuldyk"
        }
        url = reverse("forum:edit_topic", kwargs={
            'subforum_slug': self.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_first_comment = self.topic.first_comment
        response = self.client.post(url, data=data)
        self.topic.refresh_from_db()

        self.assertEqual(response.status_code, 302)
        self.assertNotEqual(self.topic.first_comment, old_first_comment)

    def test_delete_topic(self):
        url = reverse("forum:delete_topic", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_topics_count = Topic.objects.count()
        response = self.client.delete(url)

        self.assertEqual(response.status_code, 302)
        self.assertGreater(old_topics_count, Topic.objects.count())

results of testing:

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...FF..F
======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 38, in test_add_topic
    self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302

======================================================================
FAIL: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\...\forum\tests.py", line 65, in test_delete_topic
    self.assertEqual(response.status_code, 302)
AssertionError: 403 != 302

======================================================================
FAIL: test_update_first_comment (forum.tests.SubforumTestCase.test_update_first_comment)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 54, in test_update_first_comment
    self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302

----------------------------------------------------------------------
Ran 8 tests in 0.111s

FAILED (failures=3)

Tests work basing on factories; if necessary, I can provide them as well (as well as any additional data necessary to make it clear).

Try using assertRedirects() to check that (a) the view redirects and (b) that it redirects to the correct location: Testing tools | Django documentation | Django

For example:

self.assertRedirects(response, "/my-slug/topics/my-topic/", status_code=302, target_status_code=200)

The status_code and target_status_code arguments are the defaults, but it’s nice to be explicit sometimes.

Of course, my above comment doesn’t address the issue of getting a 200 when you’re expecting a 302!

In situations like this I usually find that’s because the form submission generated an error, so the view really is returning a 200, re-displaying the form with error messages.

To diagnose this you could print() the errors in the view, or in the test print(response.content) to see the HTML of the page and find the error messages. These are quite hacky low-tech solutions, and I hope there’s a better way that someone else can suggest…

Mr. Phil Gyphord,
Thank you for your answer.

The changed tests:

    def test_add_topic(self):
        data = {
            'subject': self.topic.subject,
            'first_comment': self.topic.first_comment
        }
        url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
        old_topics_count = Topic.objects.count()
        response = self.client.post(url, data=data)

        print(response)
        print(url)
        self.assertRedirects(response, f"/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
                             target_status_code=200)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(Topic.objects.count(), 2)
        self.assertGreater(Topic.objects.count(), old_topics_count)

    def test_update_first_comment(self):
        data = {
            'first_comment': "Chebuldyk"
        }
        url = reverse("forum:edit_topic", kwargs={
            'subforum_slug': self.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_first_comment = self.topic.first_comment
        response = self.client.post(url, data=data)
        self.topic.refresh_from_db()

        self.assertRedirects(response, f"/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
                             target_status_code=200)
        self.assertEqual(response.status_code, 302)
        self.assertNotEqual(self.topic.first_comment, old_first_comment)

    def test_delete_topic(self):
        url = reverse("forum:delete_topic", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_topics_count = Topic.objects.count()
        response = self.client.delete(url)

        self.assertRedirects(response, f"/{self.topic.subforum.slug}/", status_code=302, target_status_code=200)
        self.assertEqual(response.status_code, 302)
        self.assertGreater(old_topics_count, Topic.objects.count())

Here are the results of the actions you proposed:

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...<TemplateResponse status_code=200, "text/html; charset=utf-8">
FF..F
======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 39, in test_add_topic
    self.assertRedirects(response, f"/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)

======================================================================
FAIL: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 70, in test_delete_topic
    self.assertRedirects(response, f"/{self.topic.subforum.slug}/", status_code=302, target_status_code=200)
AssertionError: 403 != 302 : Response didn't redirect as expected: Response code was 403 (expected 302)

======================================================================
FAIL: test_update_first_comment (forum.tests.SubforumTestCase.test_update_first_comment)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 57, in test_update_first_comment
    self.assertRedirects(response, f"/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)

----------------------------------------------------------------------
Ran 8 tests in 0.110s

FAILED (failures=3)
Destroying test database for alias 'default'...

Is there any additional information I can provide to make the problem clear?

You could try doing this:

You might find that print(response.context["form"].errors) in your test will be most useful, but I’m not at a computer right now and can’t check.

I added the print you offered, and here are the results:

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...<ul class="errorlist"><li>subject<ul class="errorlist" id="id_subject_error"><li>Topic with this subject already exists.</li></ul></li></ul>
FE..<ul class="errorlist"><li>subject<ul class="errorlist" id="id_subject_error"><li>This field is required.</li></ul></li></ul>
F
======================================================================
ERROR: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 72, in test_delete_topic
    print(response.context["form"].errors)
          ~~~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable

======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 40, in test_add_topic
    self.assertRedirects(response, f"/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)

======================================================================
FAIL: test_update_first_comment (forum.tests.SubforumTestCase.test_update_first_comment)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 59, in test_update_first_comment
    self.assertRedirects(response, f"/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)

----------------------------------------------------------------------
Ran 8 tests in 0.111s

FAILED (failures=2, errors=1)
Destroying test database for alias 'default'...

I’ve added subject to data of test_update_first_comment, and it helped, now only two failures.

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...<ul class="errorlist"><li>subject<ul class="errorlist" id="id_subject_error"><li>Topic with this subject already exists.</li></ul></li></ul>
FF...
======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 39, in test_add_topic
    self.assertRedirects(response, f"/forum/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302,
AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302)

======================================================================
FAIL: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 72, in test_delete_topic
    self.assertRedirects(response, f"/{self.topic.subforum.slug}/", status_code=302, target_status_code=200)
AssertionError: 403 != 302 : Response didn't redirect as expected: Response code was 403 (expected 302)

----------------------------------------------------------------------
Ran 8 tests in 0.107s

FAILED (failures=2)
Destroying test database for alias 'default'...

It’s telling you there’s already a topic with that subject.

Yeah, I’ve read it, but there are not. The actual projects’ DB is empty, and testing DB is filled with factory-made data, so it can’t be.

Well, there can be and it is. Why else would the form be telling you that?

So, the error is “Topic with this subject already exists.” Which suggests that subjects must be unique within Topics (you haven’t shared model code, so I’m guessing).

In your test_add_topic() test you have self.topic.subject and you’re sending that subject as form data to the AddTopic() view.

Which suggests to me that you’re trying to create a new Topic with a subject that you’ve taken from an existing Topic (self.topic)?

Well, I’ve written at the initial that I can provide any additional information helpful, so here are models.py:

class Subforum(models.Model):
    title = models.CharField(verbose_name='Название', max_length=32, choices=consts.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):
        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)
    votes = GenericRelation(LikeDislike, related_query_name="topics_votes")
    objects = models.Manager()

    class Meta:
        ordering = ['id']
        verbose_name = 'Обсуждения'
        verbose_name_plural = 'Обсуждения'
        permissions = [
            ("edit_first_comment", "Can edit the initial message of a topic - all authorised users"),
            ("edit_topic_subject", "Can change the subject of a topic - moderators and admins"),
        ]

    def __str__(self):
        return self.subject

    def save(self, *args, **kwargs):
        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})

Here are factories.py:

class UserFactory(factory.django.DjangoModelFactory):
    username = factory.Faker('user_name')
    password = factory.Faker('password')
    email = factory.Faker('email')

    class Meta:
        model = User


class SubForumFactory(factory.django.DjangoModelFactory):
    title = factory.Faker('random_element', elements=['News',
                                                      'Characters',
                                                      'Episodes',
                                                      'References',
                                                      'Production',
                                                      'Bugs'])
    slug = factory.Faker('random_element', elements=['news',
                                                     'characters',
                                                     'episodes',
                                                     'references',
                                                     'production',
                                                     'bugs'])

    class Meta:
        model = models.Subforum


class TopicFactory(factory.django.DjangoModelFactory):
    subject = factory.Faker('sentence')
    slug = factory.Faker('slug')
    first_comment = factory.Faker('text')
    created = factory.Faker('date')
    creator = factory.SubFactory(UserFactory)
    subforum = factory.SubFactory(SubForumFactory)

    class Meta:
        model = models.Topic

I got what you mean. Now I’ll try to replace it with some random title.
I’ve tried it; well, the test_add_topic works, but without assertRedirects, as the topic’s slug is saved in the model as a slugification of a subject; but it’s not implemented in tests:

    def test_add_topic(self):
        data = {
            'subject': "Kai Cenat fanum",
            'first_comment': self.topic.first_comment
        }
        url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
        old_topics_count = Topic.objects.count()
        response = self.client.post(url, data=data)

        print(response)
        #print(response.context["form"].errors)
        self.assertRedirects(response, f"/forum/{self.topic.subforum.slug}/topics/{data['subject']}/", status_code=302, target_status_code=200)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(Topic.objects.count(), 2)
        self.assertGreater(Topic.objects.count(), old_topics_count)
======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 40, in test_add_topic
    self.assertRedirects(response, f"/forum/{self.topic.subforum.slug}/topics/{data['subject']}/", status_code=302, target_status_code=200)
AssertionError: '/forum/production/topics/topic-kai-cenat-fanum/' != '/forum/production/topics/Kai Cenat fanum/'
- /forum/production/topics/topic-kai-cenat-fanum/
?                          ^^^^^^^  ^^    ^
+ /forum/production/topics/Kai Cenat fanum/
?                          ^  ^^    ^
 : Response redirected to '/forum/production/topics/topic-kai-cenat-fanum/', expected '/forum/production/topics/Kai Cenat fanum/': Expected '/forum/production/topics/topic-kai-cenat-fanum/' to equal '/forum/production/topics/Kai Cenat fanum/'.

But if to omit assertRedirects, this test works well.
But there is still a problem with test_delete_topic and 403 code instead of 302.
P.S: well, I’ve just slugified the subject in assertRedirects, and the test was fully functional, so the only problem is now with test_delete_topic.

While I’m trying to solve the issue myself, I’m struggling to replicate a superuser in factories and tests.
factories.py:

class UserFactory(factory.django.DjangoModelFactory):
    username = factory.Faker('user_name')
    password = factory.Faker('password')
    email = factory.Faker('email')

    class Meta:
        model = User


class AdminFactory(UserFactory):
    class Params:
        superuser = factory.Trait(is_superuser=True, is_staff=True)
        enabled = True
<...>

tests.py:

class SubforumTestCase(TestCase):
    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.user = factories.UserFactory()
        self.super_admin = factories.AdminFactory()
        self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
        #self.client.force_login(self.user)
        self.client.force_login(self.super_admin)
<...>

    def test_delete_topic(self):
        url = reverse("forum:delete_topic", kwargs={
            'subforum_slug': self.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_topics_count = Topic.objects.count()
        response = self.client.post(url)

        print(response)
        #print(response.context["form"].errors)
        self.assertRedirects(response, f"/forum/{self.subforum.slug}/", status_code=302, target_status_code=200)
        self.assertEqual(response.status_code, 302)
        self.assertGreater(old_topics_count, Topic.objects.count())

Now even test_update_first_comment gets broken:

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....<HttpResponseForbidden status_code=403, "text/html; charset=utf-8">
F..F
======================================================================
FAIL: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 78, in test_delete_topic
    self.assertRedirects(response, f"/forum/{self.subforum.slug}/", status_code=302, target_status_code=200)
AssertionError: 403 != 302 : Response didn't redirect as expected: Response code was 403 (expected 302)

======================================================================
FAIL: test_update_first_comment (forum.tests.SubforumTestCase.test_update_first_comment)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 64, in test_update_first_comment
    self.assertRedirects(response, f"/forum/{self.topic.subforum.slug}/topics/{self.topic.slug}/", status_code=302, target_status_code=200)
AssertionError: 403 != 302 : Response didn't redirect as expected: Response code was 403 (expected 302)

----------------------------------------------------------------------
Ran 8 tests in 0.107s

FAILED (failures=2)
Destroying test database for alias 'default'...

P.S.: I’ve managed to handle a superuser problem - added superuser=True to the params:

    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.user = factories.UserFactory()
        self.super_admin = factories.AdminFactory(superuser=True)
        self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
        #self.client.force_login(self.user)
        self.client.force_login(self.super_admin)

But now another error appears:

Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....E...
======================================================================
ERROR: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 80, in test_delete_topic
    response = self.client.post(url, data)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^
<...>
  File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\.venv\Lib\site-packages\django\views\generic\detail.py", line 46, in get_object
    raise AttributeError(
AttributeError: Generic detail view DeleteTopic must be called with either an object pk or a slug in the URLconf.

----------------------------------------------------------------------
Ran 8 tests in 0.113s

I’ve tried to add dummy topic’s slug to data, but to no result so far:

    def test_delete_topic(self):
        data = {
            'slug': self.topic.slug
        }
        url = reverse("forum:delete_topic", kwargs={
            'subforum_slug': self.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_topics_count = Topic.objects.count()
        response = self.client.delete(url, data)

        print(response)
        #print(response.context["form"].errors)
        self.assertRedirects(response, f"/forum/{self.subforum.slug}/", status_code=302, target_status_code=200)
        self.assertEqual(response.status_code, 302)
        self.assertGreater(old_topics_count, Topic.objects.count())

I’ve never used FactoryBoy’s Traits but I think if you’re defining it like this:

Then you shouldn’t use it like this:

But like this:

self.super_admin = factories.AdminFactory(superuser=True)

And I don’t think enabled = True does anything? See the docs Reference — Factory Boy stable documentation


Regarding this error:

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

The URL is defined like this:

    path('<slug:subforum_slug>/topics/<slug:topic_slug>/delete_topic/', DeleteTopic.as_view(), name='delete_topic'),

It has two slugs there. But the view doesn’t know which one to use in order to get the Topic. By default it expects either a pk or a slug, not subforum_slug or topic_slug.

If you can get the correct Topic using only one of those, like topic_slug, then you should set the slug_url_kwarg parameter in the view:

class DeleteTopic(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Topic
    slug_url_kwarg = "topic_slug"
    # etc...

But if you require both slugs in order to find the Topic, you’ll need to override DeleteView’s get_object() method, like you have in UpdateFirstComment.

Mr. Phil Gyford,
I’ve added it, it finally worked!!! Thank you very much!!!
So, do I understand it right, that if there are several slugs in one/another endpoint’s URL, then it’s necessary to override get_object method to make it clear which slug the program should use? Or is there some other reason?
Concerning enabled = True I’m not sure, I borrowed it somewhere. I tried to delete it, but it changed nothing, so I’ve concluded that this doesn’t affect the situation.

The View classes that deal with a single object generally expect either a pk or slug to be passed in from the URL. There could be other parameters, but it will ignore those for the purposes of getting the object.

If your URL doesn’t have pk or slug parameters, then you need to tell the view how to get the object.

If you can get the object with a single unique parameter then you just need to tell the view which one to use. So if topic_slug is unique and will always only fetch a single object, you can just use slug_url_kwarg = "topic_slug" and the class’s default get_object() method will do the rest.

But if you need to use two or more parameters to get the object then you’ll need to override get_object(). So if topic_slug is only unique within each subforum, you need both topic_slug and subforum_slug to get a Topic, so override get_object().

Looking at Topic:

we can see that slug is unique so you only need that to fetch it, and don’t need to override get_object(). Just set slug_url_kwarg = "topic_slug".

I’ve tried it now with both methods you have just described, and they both worked well :slight_smile:

class DeleteTopic(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Topic
    context_object_name = 'topic'
    slug_url_kwarg = 'topic_slug'
    template_name = 'forum/topic_confirm_delete.html'
    page_title = "Удаление темы"
    fields = '__all__'

    def test_func(self):
        if self.request.user.is_superuser:
            return True
        return False

    '''
    # Necessary when there are several parameters to make clear which slug to address
    def get_object(self, queryset=None):
        return Topic.objects.get(slug=self.kwargs['topic_slug'], subforum__slug=self.kwargs['subforum_slug'])
    '''

    def get_success_url(self):
        return reverse('forum:subforum', kwargs={'subforum_slug': self.kwargs['subforum_slug']})

So, I see, if there were several not unique parameters to define endpoint’s URL, then it would require to override get_object.
Thank you!