Passing slug to CreateView

I’m trying to refactor a view from a function based view to a class based view. This view is a comment on a post, so I’m not sure if I am using the right view. Intuitively, I expected CreateView to be the right one, but it doesn’t seem to pass the slug to it:

class ProjectCommentView(CreateView):
    template_name = 'Project/project_comment.html'
    form_class = ProjectCommentForm
    queryset = Project.objects.all()
    successurl = '/home/'

    def get_object(self):
        slug = self.kwargs.get("slug")
        return get_object_or_404(Project, slug=slug)

The above code results in all the links on the page this is included in, with NoReverseMatch for links in the page. How can I fix this?

This may sound odd, but what is your original function view doing? Are you creating the comment or are you trying to display the comment? Posting your original FBV may be beneficial so we can try and diagnose the issue.

Yes, no problem this is my original function, I was just trying to build it up slowly:

def project_comment_view(request, slug):
    obj = Project.objects.get(slug=slug)
    if request.method == 'POST':
        form = ProjectCommentForm(request.POST)
        form.instance.project = obj
        if form.is_valid():
            task_complete = form.cleaned_data['task_complete']
            category = form.cleaned_data['category']
            if task_complete is True:
                CategorySatisfied.objects.create(
                        project=obj, update_category=category)
                form.save()
                return redirect('project-list', 'my-projects')
    else:
        form = ProjectCommentForm()
    context = {
        'form': form,
        'object': obj,
    }
    return render(request, 'project/project_commnet.html', context)

and while I’m here, here is the form that goes with it if that helps, it uses crispyforms:

class ProjectCommentForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = 'POST'
        self.helper.add_input(
            Submit(
                'Update',
                'Update',
                css_class='btn-primary'
            )
        )
    
    task_complete = forms.BooleanField(initial=False, required=False, label='Task Complete')

    class Meta:
        model = Comment
        fields = [
            'category',
            'comment',
        ]

Because of the extra logic you’re using for this function, you may want to just inherit from the View class that CreateView uses behind the scenes. You can use the get(self, *args, **kwargs) function replicate the function view.

Ok, thank you, that’s very useful, it was one of the things I thought maybe an issue. I’m gonna go away and try and write this from scratch.

Keep in mind that when using **kwargs, you can add the slug into the kwargs dictionary and pop it off the dictionary when it is needed.

Actually, I think one of issues is not fully understanding what the CreateView is doing. You’re trying to use “get_object” to get something other than the object you’re trying to create.

When I look at your FBV, your form is for an object of type Comment. But in your get_object method you’re trying to retrieve a Project.

Likewise, you’re setting your queryset to a queryset for Projects.

A CreateView for a Comment is focused on a Comment. All the methods and attributes should be oriented on Comment.

If you need to retrieve ancillary data related to that Comment, you can override the form_valid method. At that point in the flow, you know the form is valid and you can construct the rest of the fields for that model from any other sources of data you may need.

The Classy Class Based View web site might help you understand the flow of these CBVs better. See the CreateView for this specific case.

1 Like

Yes, correct, but it can’t stand alone, so I thought the get object was to make the correct project available to comment on. I couldn’t work out another way to do that. The user looks through the list of projects that he has an update about clicks on the title of the project and then makes his comment. I kinda followed the human behaviour to work my logic. What would be a better way to do this?

There are two different views involved here.

There’s a ListView, that would present the list of project with clickable links. Those links would direct you to the CreateView for creating the comment.

The links in the ListView could pass the slug - or could pass the primary key - of the project to be commented on as part of the URL in the link.

(This ListView followed by CreateView is a very standard Django pattern. It also happens to be the pattern followed by the Polls application used in the Django Tutorial.)

Ken

1 Like

Ah yes, I only found this forum because I literally just finished the django tutorial a couple of weeks ago for the second time to see what I’d learned in 2 years. Thank you for your input and prompt response also, let me take all that away and see where I get to.

Sorry, I have to come back to this, as I still can’t get this working in any form or fashion. What I want is a view that creates a comment on a Project that is already been created. The reason I was using the get_object method was to get the project that the comment is on. How else do you get that link to the Project I mean how is it done in other blog sites?

So I have this:
class Comment(CreateView):
model = Update
form_class = ProjectCommentForm
template_name = ‘project/project_comment.html’

url:
path('<slug:slug>/comment/', UpdateView.as_view(), name='comment')

I’m really confused, it can’t be this complicated surely?

One hook available to you for doing something like this is the form_valid method. You can query for the referenced Project in that method before the data from the form gets saved.
I believe you can use the form.instance attribute to reference the Comment object being created to assign the reference to.
(Assuming the field name in Comment is project, it would be something like:
form.instance.project = Project.objects.get(slug=self.kwargs['slug'])
I’m making some guesses here regarding names, so this is likely not exactly correct for what you need.)

Ken

Ok, It’s starting to look like what I want is just not possible with django. This all works if it is in a separate page, but it isn’t, the site I have is more like stack exchange where there is a comment form on the the post itself. I wanted to make this class based views to take advantage of using an UpdateView to be able to edit the comments. So the reason for using get_object method was to have access to the project instance as it’s required to populate the project details as well as the comment form, similar to how stack exchange is done with the post at the top, then a form to give an answer and then all the other comments underneath. So the original point of my post here is back in that I’m getting noreversematch well it’s just for one button at the moment, but if I comment that out it just moves to the next one and so on. How this kind of thing usually done?

Correct, that’s what you asked for in the previous question. You showed a CreateView in the absence of any additional information or clarification as to what you’re looking for.

Rephrasing things slightly then, it now sounds like you also want to display some additional information on the same page as the comment form.
That feature is implemented by overriding the get_context_data method, and adding whatever data you want displayed on that page. (Your template would then render that content however you choose.)

Right, now I understand, that is what context is actually for. Context is a really tricky subject to understand.

Yes my written style is not good, I’ve been working on this site for a year, and it just went into production last month, I’m just trying to now refactor it using and learning best practices if I can, as I have another one to do and I don’t want that one to take a year. I’ll try to ask my questions better in future. Thankyou very much for your help though, I’m very glad I found this forum

While I can understand the confusion with the name “context”, conceptually, it’s probably easier than you may think it is. In the simplest terms, your context is the data being used to populate the variables in a template. Whenever you have a reference in a template such as {{ xyz }} the template rendering engine looks at the context to find xyz.

And please, don’t worry about your writing style - it’s really quite fine. It’s not that the questions aren’t well written, it’s only that they weren’t complete for where you were headed - and that’s not necessarily a bad thing. Sometimes breaking a larger problem into smaller easier-to-understand problems is quite beneficial toward understanding the whole. However, when you take that approach, you should recognize that the answers to questions may change as a result of adding in that additional functionality. (It doesn’t in this case - handling the POST form for linking the comment to the original object occurs separately and independently of the GET displaying that form with other data.)

Ken

1 Like

Sorry about this, I’m nearly there, just stuck on one small thing now, on submit, the foreign key from the project is not added so the project the comment is connected to is blank. I have this so far:

class CommentView(CreateView):
    model = Comment
    form_class = ProjectCommnetForm
    template_name = 'project_portal/project_comment.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['project'] = Project.objects.get(slug=self.kwargs['slug'])
        return context
    
    def form_valid(self, form):
        form.instance.project_id = project.id
        return super(CommentView, self).form_valid(form)

I tried using form_valid method in there, but it didn’t work, and looking on the ccbv.co.uk for createview, it does look like the get_context_data is supposed to deal with that, so without the form_valid method, the project_id is being returned as null, with the form_valid() method, I get ‘project’ is not defined. What am I doing wrong here please?

They’re two different functions that occur in two different steps of the lifecycle.

  1. Display the original form - if you follow the sequence of execution for the get method, you’ll see it ends up calling get_context_data before rendering the form in the template.
  2. Process the submitted form - the post method binds the data to the form and then calls form_valid. I’m not seeing where get_context_data is involved in this process.

In the same way that you have the following line in get_context_data:
context['project'] = Project.objects.get(slug=self.kwargs['slug'])

you could do something like this in form_valid, assuming your post url is passing the slug in as a parameter on the post:
project = Project.objects.get(slug=self.kwargs['slug'])

Another way of handling this would be to pass the current project.id value as a hidden field in the form, and just refer to that field in the form_valid method.

Always keep in mind that in the flow of events, a GET of a form is completely independent of a POST of that same form. Even if you’re using the same View class to handle both, they are different instances being created to handle each call. You can’t pass data between the GET and the POST through the instance of a View class.
What that really means then is that to get data from the GET to the POST, it needs to be stored external to the class - in this case, when you’re talking about forms, one of the three common ways of doing it is through form variables. (The database and session data are the other two - neither of which are particularly appropriate in this case.)

Ok, just one last question, and if I still get errors, I’m sacking this off and sticking to the function based view, but for some reason, it’s now not redirecting back to the same page like it was, now it’s sending one of the form fields as an argument, but I can’t find how the createview redirects, my original FBV just went back to the same page which is the desired behaviour, I’m getting really confused, why is it sending one of the form fields as an argument?