Django problems with UpdateView and DeleteView implementaion inside the app

I have a forum app, its structure is “forum-subforum-topic-comments”, with topics exclusive for every subforum and comments exclusive for every topic.
Finally having implemented the AddTopic/AddComment functional, I faced a problem of links for editing the comment inside the topic (UpdateView) and deleting the comment (DeleteView) being disfunctional. I had written them via <a href="<url_pattern_name>></a>", so I re-wrote them with url tag (including all the parameters from urls’ path; I don’t know whether it is an optimal solution, but it works, at least).

Links have finally become functional and sent me to the edit page, but when I wanted to submit changes, the program returned an error:

TypeError at /forum/<subforum_slug>/topics/<topic_slug>/45/edit_comment/
Field 'id' expected a number but got <Comment: Post of <topic_subject> is posted by <user>.>.
Request Method:	POST
Request URL:	http://127.0.0.1:8000/forum/<subforum_slug>/topics/<topic_slug>/45/edit_comment/
Django Version:	5.1.1
Exception Type:	TypeError
Exception Value:	
Field 'id' expected a number but got <Comment: Post of <topic_subject> is posted by <user>.>
Exception Location:	D:\<path>\django_project\.venv\Lib\site-packages\django\db\models\fields\__init__.py, line 2125, in get_prep_value
Raised during:	forum.views.UpdateComment
Python Executable:	D:\PyCharm Community Edition 2024.1.3\PycharmProjects\django_project\.venv\Scripts\python.exe
Python Version:	3.12.3

(<subforum_slug>, <topic_slug>, <topic_subject>, , - my replacements to make it clear)

The string it has received instead somehow repeats the return of my __str__ method in Comment model; I’ve tried to change it into return self.id, but it returned an error with statement that it wanted a str object, not int (which is understandable, as far as it is a __str__ method).

And here I got lost. I’ve googled for it and changed all my "id"s in self.kwargs[''] to "pk"s, but it didn’t help in this case. So I need help from the community.

(I thought that Django would be easier to master)

urls.py:

from django.urls import path

from forum.views import *


app_name = 'forum'

urlpatterns = [
    #path('<slug:profile_slug>/', user_profile, name='user_profile'),
    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>/add_comment/', AddComment.as_view(), name="add_comment"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/<int:pk>/edit_comment/', UpdateComment.as_view(), name="edit_comment"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/<int:pk>/delete_comment/', DeleteComment.as_view(), name="delete_comment"),
]

views.py:

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

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

    def form_valid(self, form):
        topic = Topic.objects.get(slug=self.kwargs['topic_slug'])
        form.instance.author = self.request.user
        form.instance.topic = topic
        return super(AddComment, self).form_valid(form)


class UpdateComment(LoginRequiredMixin, DataMixin, UpdateView):
    model = Comment
    form_class = AddCommentForm
    context_object_name = 'comment'
    template_name = 'forum/editcomment.html'
    page_title = 'Редактировать комментарий'

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

    def form_valid(self, form):
        topic = Topic.objects.get(slug=self.kwargs['topic_slug'])
        comment_id = Comment.objects.get(id=self.kwargs['pk'])
        form.instance.author = self.request.user
        form.instance.topic = topic
        form.instance.id = comment_id
        return super(UpdateComment, self).form_valid(form)


class DeleteComment(LoginRequiredMixin, DataMixin, UpdateView):
    model = Comment
    context_object_name = 'comment'
    template_name = 'forum/comment_confirm_delete.html'
    page_title = "Удаление комментария"

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

    def form_valid(self, form):
        topic = Topic.objects.get(slug=self.kwargs['topic_slug'])
        form.instance.author = self.request.user
        form.instance.topic = topic
        return super(DeleteComment, self).form_valid(form)

models.py:

<...>
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}.'

topic.html:

{% extends 'base.html' %}
{% load static %}

{% block content %}
<h1>{{ topic.subject }}</h1>

<div class="container-posts">
  <div class="row">
    <div class="card-subtitle">Создал: {{ topic.creator }}, дата создания: {{ topic.created }}</div>
    <div class="card-subtitle">Всего комментариев в теме: {{ comm_num }}</div>
    <div class="container-posts">
        <a href="add_comment"><button>Оставить комментарий</button></a>
    </div>
    <div class="card-body">
      <div class="media">
        {% if p.photo %}
          <img src="{{p.photo.url}}" alt="" width="300" height="300">
        {% else %}
          <img src="{% static 'core/images/Swr-portrait-aya.png' %}" alt="" width="150" height="150">
        {% endif %}
      </div>
      <h5 class="card-subtitle">{{ topic.creator }}</h5>
      <h5 class="card-subtitle">Создан: {{topic.created|date:"d.m.Y H:i"}}</h5>
      {% autoescape off %}
      <p class="card-text">{{topic.first_comment|linebreaks|truncatewords:200}}</p>
      {% endautoescape %}
      <a href="{% url 'forum:edit_comment' topic.subforum.slug topic.slug %}">Редактировать</a>
      <div class="clear"></div>
      <hr>
    </div>

    <!--Comments section -->
    {% for p in comments %}
      <div class="card-posts">
        <div class="card-body">
          <div class="media">
            {% if p.photo %}
              <img src="{{p.photo.url}}" alt="" width="300" height="300">
            {% else %}
              <img src="{% static 'core/images/Swr-portrait-aya.png' %}" alt="" width="150" height="150">
            {% endif %}
          </div>
          <h5 class="card-subtitle">{{ p.author }}</h5>
          <h5 class="card-subtitle">Создан: {{p.created|date:"d.m.Y H:i"}}</h5>
          {% autoescape off %}
          <p class="card-text">{{p.content|linebreaks|truncatewords:200}}</p>
          {% endautoescape %}
          <div class="card-subtitle">
            <a href="{% url 'forum:edit_comment' topic.subforum.slug topic.slug p.id %}">Редактировать</a>
            <a href="{% url 'forum:delete_comment' topic.subforum.slug topic.slug p.id %}">Удалить</a></div>
          <div class="clear"></div>
          <hr>
        </div>
      </div>
    {% endfor %}

  </div>
</div>
{% endblock %}

editcomment.html:

{% extends 'base.html' %}

{% block content %}
<h1>Edit comment</h1>

<div class="container-posts">
    <form action="" method="post">
        {% csrf_token %}
        <div class="form-error">{{ form.non_field_errors }}</div>
        {% for f in form %}
            <div class="container-posts">
                <p><label class="form-label" for="{{ f.id_for_label }}">{{ f.label }}</label>{{ f }}</p>
                <div class="form-error">{{ f.errors }}</div>
            </div>
        {% endfor %}
        <p><button type="submit">Edit comment</button></p>
    </form>
</div>
{% endblock %}

comment_confirm_delete.html:

{% extends 'base.html' %}

{% block content %}
<h1>Delete topic</h1>

<div class="container-posts">
    <form method="post">
        {% csrf_token %}
        <div class="form-error">{{ form.non_field_errors }}</div>
        <p>Are you sure you want to delete "{{ object }}"?</p>
        <p><button type="submit">Delete</button></p>
        <a href="topic">Cancel</a>
    </form>
</div>
{% endblock %}

As always, if any additional info is required, I’m ready to provide it.

You cut off the error message so we can’t see which of the many lines of code it relates to.

The basic problem is that “<Comment: Post of <topic_subject> is posted by .>” can not be interpreted as an integer. Which I think you also agree with!

But there’s a bunch of other problems too like

Request URL:	http://127.0.0.1:8000/forum/<subforum_slug>/topics/<topic_slug>/45/edit_comment/

The literal string <subforum_slug> is in the browser URL! That’s clearly very very wrong.

You should not be using reverse() if you don’t understand the basics enough to immediately see how wrong that URL is.

Mr. Boxed,
Specially for that case I wrote, that <subforum_slug> and others are my replacements to make clear the logic (and not to make others cringe from my subforum and topics’ names).

But if you want it in genuine state, here the error as it is (already edited back, hehe):

TypeError at /forum/<subforum_slug>/topics/<topic_slug>/45/edit_comment/
Field 'id' expected a number but got <Comment: Post of <topic_subject> is posted by <user>.>.
Request Method:	POST
Request URL:	http://127.0.0.1:8000/forum/production/topics/topic-scenario-timing-plan/45/edit_comment/
Django Version:	5.1.1
Exception Type:	TypeError
Exception Value:	
Field 'id' expected a number but got <Comment: Post of <topic_subject> is posted by <user>.>.
Exception Location:	<path>\django_project\.venv\Lib\site-packages\django\db\models\fields\__init__.py, line 2125, in get_prep_value
Raised during:	forum.views.UpdateComment
Python Executable:	D:\PyCharm Community Edition 2024.1.3\PycharmProjects\django_project\.venv\Scripts\python.exe
Python Version:	3.12.3
<...>
Traceback Switch to copy-and-paste view
<...>
The above exception (int() argument must be a string, a bytes-like object or a real number, not 'Comment') was the direct cause of the following exception:
<...>
Local vars
<path>\django_project\forum\views.py, line 110, in form_valid
        return super(UpdateComment, self).form_valid(form)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ …
<...>

comment_id here does not contain the id of the Comment object. You’re executing a query to retrieve the Comment object, and so comment_id is a reference to an object of type Comment

form.instance.id needs to have the id assigned, not a Comment.

1 Like

Ken beat me to it heh.

I want to add to his statement:

comment_id = Comment.objects.get(id=self.kwargs['pk'])  # <- BAD

This line is a lie. The ID of a thing is not the same as the thing itself. I HAVE a name, but my name when written on a paper can’t eat food, or breathe. This is an extremely important distinction that you must keep very clear in your mind.

comment_id = self.kwargs['pk']  # <- GOOD
comment = Comment.objects.get(id=self.kwargs['pk'])  # <- GOOD

In general I would recommend that in the future, don’t alter information you send to someone trying to help you. The slightest details often matter hugely in programming, so if you modify what you send, you might destroy the important information, or make us believe you have other problems.

1 Like

Mr. Boxed,
Okay, I got your point. Now I will hide that error’s text, as it contains some personal info I wouldn’t like to disclose (if you understand me).
Thank you very much.

If you really need to censor, which does happen, I would recommend writing it like

http://*****/forum/****/topics/

This is commonly understood as censored information.

1 Like

Okay, somehow my question contained the answer in itself - DeleteView’s do not take any forms, so form_valid function is needless for it.
Thank you, gentlemen.