Passing pk of other model through CreateView

I’m having no fun with a simple CreateView issue, which has been so frustrating that I’m seriously considering ditching class-based views entirely (so that at least I would understand what I don’t know and could ask a better question).

What I think I want to do is to pass the primary key of the Task model through the steps below, so that each instance of the Note model is linked to the correct instance of the Task model.

I have a Task model and a Note model. The Note model has a field, task, which is a primary key to the Task model:

 class Note(models.Model):
     task = models.ForeignKey(
         Task, on_delete=models.CASCADE, related_name="notes", null=False, blank=False
     )  # Required, links to tasks.Task
     text = models.TextField(
        null=False, blank=False,
    )  # Required

The TaskDetailView renders a template (task_detail.html) which includes a link to add new note:
<a href="{% url 'tasks:note_new' task.id %}" class="btn btn-primary">Add Note</a>

The application urls.py file directs this request (and the argument) as follows:
path("note/<int:task_id>/", NoteCreateView.as_view(), name="note_new"),

The NoteCreateView in views.py is:

class NoteCreateView(CreateView):
    model = Note
    form_class = NoteForm
    template_name = "tasks/note_detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["page_title"] = "New Note"
        return context

    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.task_id = self.kwargs.get("task_id")
        print("\nin form_valid: ", self.kwargs.get("task_id"))
        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        return reverse_lazy('tasks:edit', kwargs={'pk': self.kwargs.get("task_id")})

This renders a new, small ModelForm:

forms.py:

class NoteForm(ModelForm):
    
    class Meta:
    
        model = Taskmodel = Note
        fields = ["text"]
        widgets = {
            "text": forms.TextInput(attrs={
                    "class": "input input-primary"
                 }
                )
            }

note_detail.html:

        <form action="{% url 'tasks:note_new' task_id=task.id %}" method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit" value="Save" class="btn btn-accent" />
        </form>

I think the current error is:

NoReverseMatch at /tasks/note/85/

Reverse for ‘note_new’ with keyword arguments ‘{‘task_id’: ‘’}’ not found. 1 pattern(s) tried: [‘tasks/note/(?P<task_id>[0-9]+)/\Z’]

85 is the correct pk for the task. And the traceback points to the form definition and url in note_detail.html as the cause of the error.

My suspicion is that the task pk is not being passed from the first page (which renders task_detail.html and provides the link to CreateView), so the template for the CreateView can’t reverse the URL back to the originating task. I can’t seem to dig through the layers of abstraction in the CBV to work out why or how and where to override it, though.

Can you share the relevant urls.py files?

You are using POST method with URL?

<form method="POST"> 
</form>

The above should suffice. CreateView's post method will still handle the POST

Thanks for taking a look and sure thing. It’s below. I’ve trimmed out a few task model only list views that aren’t related to this issue.

app_name = "tasks"

urlpatterns = [
    path("<int:pk>/delete/", TaskDeleteView.as_view(), name="delete"),
    path("<int:pk>/", TaskUpdateView.as_view(), name="edit"),
    path("note/<int:task_id>/", NoteCreateView.as_view(), name="note_new"),
    path("note/<int:pk>/", NoteDetailView.as_view(), name="note_detail"),
    path("new/", TaskCreateView.as_view(), name="new"),
...
    path("", TaskListView.as_view(), name="list"),
    path("", TaskListView.as_view(), name="index"),
]

The note creation form uses POST, yes:

<form action="{% url 'tasks:note_new' task_id=task.id %}" method="post">

Though the link that calls that note creation form would be a GET request:

<a href="{% url 'tasks:note_new' task.id %}" class="btn btn-primary">Add Note</a>

Those tracebacks might be valuable for advising you. Can you share those too?

# NoReverseMatch at /tasks/note/85/

Reverse for 'note_new' with keyword arguments '{'task_id': ''}' not found. 1 pattern(s) tried: ['tasks/note/(?P<task_id>[0-9]+)/\\Z']

|Request Method:|GET|
| --- | --- |
|Request URL:|http://127.0.0.1:8000/tasks/note/85/|
|Django Version:|5.1|
|Exception Type:|NoReverseMatch|
|Exception Value:|Reverse for 'note_new' with keyword arguments '{'task_id': ''}' not found. 1 pattern(s) tried: ['tasks/note/(?P<task_id>[0-9]+)/\\Z']|
|Exception Location:|/xxxx/venv-house/lib/python3.12/site-packages/django/urls/resolvers.py, line 831, in _reverse_with_prefix|
|Raised during:|tasks.views.NoteCreateView|
|Python Executable:|/xxxx/venv-house/bin/python|
|Python Version:|3.12.7|
|Python Path:|```
['/Users/xxxx/house',
 '/usr/local/Cellar/python@3.12/3.12.7_1/Frameworks/Python.framework/Versions/3.12/lib/python312.zip',
 '/usr/local/Cellar/python@3.12/3.12.7_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12',
 '/usr/local/Cellar/python@3.12/3.12.7_1/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload',
 '/Users/xxxx//venv-house/lib/python3.12/site-packages']
```|

Error during template rendering

In template /Users/xxxx/house/tasks/templates/tasks/note_detail.html, error at line 20
Reverse for 'note_new' with keyword arguments '{'task_id': ''}' not found. 1 pattern(s) tried: ['tasks/note/(?P<task_id>[0-9]+)/\\Z']
10	            {% if form.non_field_errors %}<span class="text-error">{{ form.non_field_errors }}</span>{% endif %}
11	            {% for field in form %}
12	                {% for error in field.errors %}<span class="text-error">{{ error }}</span>{% endfor %}
13	            {% endfor %}
14	        </div>
15	    {% endif %}
16	    <div class="flex flex-row flex-wrap gap-5 p-3">
17	        <p>task.id: {{ task.id }}</p>
18	        <p>note.task: {{ note.task }}</p>
19	        <p>task.notes: {{ task.notes }}</p>
20	        <form action="{% url 'tasks:note_new' task_id=task.id %}" method="post">
21	            {% csrf_token %}
22	            {{ form.as_p }}
23	            <input type="submit" value="Save" class="btn btn-accent" />
24	        </form>
25	    </div>
26	{% endblock main %}
27	

I do not see where you passed the task object to the view, therefore the template doesn’t see it

Yeah, that’s what I thought this task.id argument was doing … at least as far as displaying the form that goes into the CreateView when complete:

This will still fail if task is not in the context dictionary.

What do you see on the console when you modify get_context_data like so?:

def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["page_title"] = "New Note"
        print(context)
        return context

If you do not see “task” in the context, that is the problem

Thanks onyeibo

I get:
{'form': <NoteForm bound=False, valid=Unknown, fields=(text)>, 'view': <tasks.views.NoteCreateView object at 0x111825b50>, 'page_title': 'New Note'}

So, no task in the context. I had wondered if this was where the gap was …

I also have Django-debug-toolbar installed, if that’s helpful.

So let me understand the flow here:

You want to create a note that references a task. The url pattern associated with the CreateView is: path("note/<int:task_id>/", NoteCreateView.as_view(), name="note_new"),

Here is what is happening:
The URL calls the view with task_id which passes to the get method in the CreateView. It arrives the view inside a kwargs dictionary. It stays there waiting for you to use it, but your code ignores it completely.

CreateView is instantiating a Note object or model

class NoteCreateView(CreateView):
    model = Note

Remember that. The View is not creating any task instance as expected. It only recieves the id (task_id) which arrives in kwargs. You need to pass that task_id into the context. Here is how:

def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["page_title"] = "New Note"
        context["task_id"] = self.kwargs.get("task_id")
        print(context)
        return context

You should now see the task_id in the context with the correct id value.

But wait!!!
Why do you need to pass the task_id to create a Note?. It is totally unneccessary. CreateView does not need the task_id to create a new Note object. Your form specifies only the “text” field (that’s ok). Therefore, the form will render only the text widget. When you submit, the post method will pick it up and validate it. Then it will pass control to your form_valid method where you assign task_id

modify your note_detail.html to solve the problem because it is calling CreateView again (loop)

        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit" value="Save" class="btn btn-accent" />
        </form>

That should do it

Many thanks onyeibo - for the fix and help to understand the problem (that I had caused by trying to fill in a gap that CBV had already filled!).

So, and this is where I think I’m confused by the CBV abstraction, how is the view able to access kwarg to get the original task, so that task.id is available to the form_valid method?

Is there anywhere you can suggest that I can try to follow the flow through the CBV to understand what it has and where ? I’ve looked at classy class-based views and the flowcharts, and I’m not finding either very helpful. I sort of need the classy class-based views information but sorted in the order that the various methods are called, and I can’t piece together the various layers of class inheritance to understand the order in which things happen. The source code is available, of course, but not that accessible.

When looking at a view on ccbv like CreateView -- Classy CBV you start with dispatch(), which will then generally call either get() or post().

It takes time to get used to new tech.

If you are comfortable with ordinary Python classes, CBV is joy to work with. CBVs handle a lot of common logic/flow.

The challenge with CBVs is knowing the right class for the task. Most of them follow a common flow.

  1. GET. When you request a URL/Page, the get() method is usually the place to see parameters passed to the view. Look in *args and **kwargs for id or pk etc.
  2. The get() method will pass control to get_context_data() in most cases. This is where the view reads extra_context that it will add to the render context. If the view needs a form, this method adds that too.
  3. render_to_response() is next. It recieves the context and sends collected data to the browser for rendering
  4. At the browser … you fill form (if there is one). A page with forms often need the POST method. When you hit “submit” button, Some of the data will be validated by the browser. If everything goes well, the POST executes.
  5. The data arrives back to the view at post() or get() depending on the method on the form. For forms, it should be POST although GET can still work. THe difference is that GET inserts the data into the URL as queries so that people see everything on the address bar. Most times, you dont want that, unless you really want URL queries. At Post(), the view validates the form further. If everything is okay, the data moves to form_valid() … otherwise, it goes to form_invalid().
  6. At form_valid() you decide what to do with the validated form … usually a form.save() which saves associated and updated instances into the database for ModelForms. At form_invalid(), the data routes back to get_context_data() for another trial instead.
  7. After saving the data (for successful cycles), the view is done. It will pass control to another URL that you specify in success_url

That is the summary. It varies slightly with CBVs. The above is the typical flow for CreateView and FormView (or any view with FormMixins)

Check out CCBV for the variants.

Goodluck and happy experimenting

I looked at CCBV, but I cant figure out how to see “you start with dispatch”, maybe I am missing some understanding of how it it trying yo help me?

Samy kind of question as to Philgyford. How do you see that on CCBV?

It is not a tutorial. It is more of a source documentation. @philgyford was offering a personal advise from his experience. Understanding that resource requires patience and experience with Python Classes. Perhaps, I will look for a flow chart and share. I personally learned by reading the code at CCBV

1 Like

Thank you for explaining

Thanks again @onyeibo and @philgyford - this will be very helpful. Appreciate the help and suggestions!

3 Likes