Autoset Foreign key data for Django forms

I have a forum app, its structure is “forum → subforum → topic → comment”, and now I’m trying to implement the inner (not via admin page) addition of topics and comments. As I also have an authorization system (by embedded User class), after successful authorization the username must be autosubstituted in case of creating a new topic/comment.
Well, if I was able to google down to solution for “user” field (form.instance.author = self.request.user), then in case of comment addition this scheme doesn’t work, because it demands a “topic” Foreign Key, which is, somehow, not so easy to obtain. With my desperate googling (e.g., here: Automatically Foreign Key in Form. How?? - #3 by nimdagh) I’ve tried something like that:

AddComment view from views.py:

class AddComment(LoginRequiredMixin, DataMixin, CreateView):
    model = Comment
    form_class = AddCommentForm
    template_name = 'forum/addcomment.html'
    page_title = 'Leave a comment'
    success_url = reverse_lazy('topic')

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

But it returned me a KeyError:

KeyError at /forum/production/topics/topic-scenario-timing-plan/add_comment/
'topic_id'
Request Method:	POST
Request URL:	http://127.0.0.1:8000/forum/production/topics/topic-scenario-timing-plan/add_comment/
Django Version:	5.1.1
Exception Type:	KeyError
Exception Value:	
'topic_id'
Exception Location:	<path>\django_project\forum\views.py, line 71, in form_valid
Raised during:	forum.views.AddComment
Python Executable:	D:\PyCharm Community Edition 2024.1.3\PycharmProjects\django_project\.venv\Scripts\python.exe
Python Version: 3.12.3

I’ve tried to use lookups (topic = self.model.objects.get(topic__id=self.kwargs['topic_id'])), but they weren’t useful as well. What’s more interesting, when I used a field that wasn’t included (like, topic = self.model.objects.get(topic_slug=self.kwargs['topic_slug'])), the program returned an error, suggesting me a list of fields, among which were both “topic”, and “topic_id”. I’d really like to see what the hell is going on inside those “self.kwargs”, but cannot achieve them.
My question(s) is/are:

  • What is the optimal way to implement this automatical setting of Foreign keys inside Django forms (only authorised users may create a topic and leave comments, so for comments addition there must be a User and Topic foreign keys)?
  • (Optional) How do those “get_form_kwargs”, “get_initial”, “form_valid” and alike work with forms, what is better to use basing on the situation? I’ve searched through a number of sources, Django docs as well, and noone of them approaches this problem scrupulous enough.
    forms.py:
from django import forms
from django.core.exceptions import ValidationError

from forum import models


class AddTopicForm(forms.ModelForm):
    subject = forms.CharField(label="Заголовок", max_length=100, min_length=7)
    first_comment = forms.CharField(label="Сообщение", widget=forms.Textarea())

    class Meta:
        model = models.Topic
        fields = ['subject', 'first_comment']

    def clean_subject(self):
        subject = self.cleaned_data['subject']
        if len(subject) > 100:
            raise ValidationError("Длина превышает 100 символов")
        if len(subject) < 7:
            raise ValidationError("Слишком короткое заглавие, требуется не менее 7 символов")
        return subject


class AddCommentForm(forms.ModelForm):
    content = forms.CharField(label="Text", max_length=2000, min_length=1, widget=forms.Textarea())

    class Meta:
        model = models.Comment
        fields = ['content']

Topic and Comment models from 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}.'

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>/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>/edit/<int:id>/', UpdateComment.as_view(), name="edit_comment"),

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

Does this code perhaps crash?

Anyway, you want something like topic = self.model.objects.get(slug=self.kwargs['topic_slug']) I think

1 Like

Boxed,
Yes, precisely this piece (at least, on this stage).
If I recall that right, I’ve already tried the code you’ve suggested, but I’ll try it once more.

Yep, I’ve already had that error:

FieldError at /forum/production/topics/topic-scenario-timing-plan/add_comment/
Cannot resolve keyword 'slug' into field. Choices are: author, author_id, content, created, id, topic, topic_id, updated
Request Method:	POST
Request URL:	http://127.0.0.1:8000/forum/production/topics/topic-scenario-timing-plan/add_comment/
Django Version:	5.1.1
Exception Type:	FieldError
Exception Value:	
Cannot resolve keyword 'slug' into field. Choices are: author, author_id, content, created, id, topic, topic_id, updated
Exception Location:	<path>\django_project\.venv\Lib\site-packages\django\db\models\sql\query.py, line 1768, in names_to_path
Raised during:	forum.views.AddComment
Python Executable:	D:\PyCharm Community Edition 2024.1.3\PycharmProjects\django_project\.venv\Scripts\python.exe
Python Version:	3.12.3

oh, self.model here is Comment… CBVs are so confusing.

topic = self.model.objects.get(topic=self.kwargs['topic'])

this line has then multiple problems. self.model is not Topic, but Comment, so it should be:

comment = self.model.objects.get(topic__slug=self.kwargs['topic_slug'])

presumably

1 Like

…which I realize now is probably also incorrect, because it’s a class called AddComment, so what you may want is

topic = Topic.objects.get(slug=self.kwargs['topic_slug'])

I think I’ll have to recommend not using self.model but instead write the real model name there, as it’s super confusing otherwise.

1 Like

And yet another error, which I’ve already met as well:

NoReverseMatch at /forum/production/topics/topic-scenario-timing-plan/add_comment/
Reverse for 'topic' not found. 'topic' is not a valid view function or pattern name.
Request Method:	POST
Request URL:	http://127.0.0.1:8000/forum/production/topics/topic-scenario-timing-plan/add_comment/
Django Version:	5.1.1
Exception Type:	NoReverseMatch
Exception Value:	
Reverse for 'topic' not found. 'topic' is not a valid view function or pattern name.
Exception Location:	<path>\django_project\.venv\Lib\site-packages\django\urls\resolvers.py, line 831, in _reverse_with_prefix
Raised during:	forum.views.AddComment
Python Executable:	D:\PyCharm Community Edition 2024.1.3\PycharmProjects\django_project\.venv\Scripts\python.exe
Python Version:	3.12.3

Concerning the direct use of models - indeed, it’s only because I had a problem with circular import in the same project, so I’m kind of afraid to use imported models now :slight_smile:

My personal opinion, which is controversial, is: don’t bother with reverse() unless you have a good reason to use it.

In this case I’d have to guess you have to do app_name:topic and not just topic.

1 Like

This is a completely separate issue, caused by this:

You do not have a url defined with the name 'topic' that doesn’t take any parameters. Your 'topic' url takes two parameters, subforum_slug and topic_slug.
Since those are variables, instead of defining the variable success_url, you will want to override the get_success_url method to call the reverse function on that url name, supplying those two parameters.

Also, since your url has the app_name = forum attribute, the proper reference to the view is 'forum:topic'

This is frequently caused by a sub-optimal organization of the models in your project. When this occurs, my first thought is always to look at the project organization to see if there’s something to be improved, but that’s probably not a topic we want to get into here.

1 Like

Mr. KenWhitesell,
Thank you very much, I’ll try that,

Indeed! The problem is hidden in the clause “to see if there’s something to be improved”, which is hardly possible if there’s no clear understanding of what and how to improve.

Do you have any insight about the second (optional) question? Or, at least, is there any proper tutorial for Django, that covers the mentioned problem and usage of mentioned functions?

What I would actually suggest is that you open another topic for this. It’s a very context-sensitive topic, which means the options and solutions depend a lot upon the specific project involved. There’s no “one-size-fits-all” answer for all possible projects.

1 Like

Mr. KenWhitesell,

A further update:
I wrote something like this:

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

    def get_success_url(self):
        #reverse('forum:topic', args=['subforum_slug', 'topic_slug'])
        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)

and the program returns not a redirect, but THIS URL (and 404 error):

http://127.0.0.1:8000/forum/production/topics/topic-scenario-timing-plan/add_comment/None

(Instead it should do http://127.0.0.1:8000/forum/production/topics/topic-scenario-timing-plan)
And I still don’t understand, what is going on inside those self.kwargs, as I don’t know, which key to take from them.

Could you explain, please?

They match directly from the variables defined in the urls.

For example, for one url you have defined:

This is going to create three entries in the kwargs attribute, subforum_slug, topic_slug, and id. This means, that in this view (UpdateComment), you can access self.kwargs['subforum_slug'], self.kwargs['topic_slug'], and self.kwargs['id'].

Regarding the current error - is the urls.py file contents you posted back at the very beginning still correct? Or have you made any changes since then?
Are there more definitions in this urlpatterns after these entries?

Do you have any other urls defined with the name topic?

I’m assuming that this urlpatterns is being included in a “higher level” urlpatterns? If so, it may be helpful to see those higher level definitions.

1 Like

Mr. KenWhitesell,
Yes, yes, yes, finally I’ve learned what lies in these notorious self.kwargs, this sorrowful guess-game has finally ended. Thank you very much.

Since the last topic, where you advised me to insert an additional element to paths to avoid the risk of overlap between “add_topic” and “topic_slug” (and what I have implemented from your advice), I’ve made no changes into forum/urls.py.
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>/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>/edit/<int:id>/', UpdateComment.as_view(), name="edit_comment"),
]

<project_name>/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 necessary, I’m here and ready to provide it.

Ok, silly me, I missed the issue here:

The get_success_url needs to return the url, making the correct line:
return reverse(...

Mr. KenWhitesell,
Hahaha, if you call yourself silly, then how I should be called:DDD
My mistake, my oversight.

Corrected it, now it finally works.

Thank you very much, Mr. KenWhitesell, it wouldn’t have ended without your advices.