Search code examples
pythondjangodjango-viewshttp-status-code-404

Why is django not finding any object matching the query, even though object is created


HEADSUP: I don't know if this is a duplicate. There are many with similar titles one, but they aren't answering my question. If it is a duplicate, however, please inform me.

I have this model called Item:

class Item(models.Model):
    title = models.CharField(max_length=120)
    date_created = models.DateTimeField(default=timezone.now)
    deadline = models.DateTimeField(default=timezone.now() + timedelta(5))
    task = models.ForeignKey(Task, on_delete=models.CASCADE)
    is_completed = models.BooleanField(default=False)

If you see, Item is referencing a ForeignKey called Task:

class Task(models.Model):
    title = models.CharField(max_length=120)
    description = models.TextField()
    date_created = models.DateTimeField(default=timezone.now)
    deadline = models.DateTimeField(default=timezone.now() + timedelta(5))
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    is_completed = models.BooleanField(default=False)

I have a delete view for Item:

class DeleteItemView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Item
    template_name = 'function/delete_item.html'
    success_url = reverse('all-items')

    def test_func(self):
        item = get_object_or_404(Item, id=self.kwargs['pk2'])
        if item.task.author == self.request.user and not item.task.is_completed:
            return True
        return False

    def get_context_data(self, **kwargs):
        context = super(DeleteItemView, self).get_context_data(**kwargs)
        context['task'] = Task.objects.filter(id=self.kwargs['pk'])[0]
        return context

The url pattern for the delete view is:

    path('task/<int:pk>/item/<int:pk2>/delete/', DeleteItemView.as_view(), name='delete-item'),

pk is for the Task model and pk2 is for the Item model.

Now, my problem is this. Whenever I try to go to this delete view, it results in an error, saying there is no item found matching the query (404). And it says Raised by: function.views.DeleteItemView. I am at my wits end, spending almost half a day trying to figure this out, but with no success.

Any help or answers will be immensely and greatly appreciated.


Solution

  • The problem here is a lack of understanding of class based views that deal with single objects. Any view that deals with a single object inherits from SingleObjectMixin [Django docs] down the line. SingleObjectMixin gets the relevant object using either (or both) the pk or slug passed as keyword arguments to the view.

    The name of the kwarg for pk is denoted by the pk_url_kwarg which defaults to pk, (meaning if there is a kwarg pk passed to the view it will be used for getting the object) and the kwarg for the slug is denoted by slug_url_kwarg.

    In your view you deal with two objects one Item and the other Task. And the generic view is supposed to deal with the Item. The problem here is you pass the pk for the Item as pk2 and for the Task as pk! The view of course considers that pk is for the instance it needs to deal with and hence you get the error.

    One solution would be to interchange the names of the two kwargs, but we can do better than that. pk and pk2 are not very descriptive, yes? Lets change their names to be more descriptive.

    In your url pattern, change the names of the captured arguments:

    path('task/<int:task_pk>/item/<int:item_pk>/delete/', DeleteItemView.as_view(), name='delete-item'),
    

    In your view, set pk_url_kwarg and also correct the names:

    class DeleteItemView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
        model = Item
        template_name = 'function/delete_item.html'
        success_url = reverse('all-items')
        pk_url_kwarg = 'item_pk'  # set this
    
        def test_func(self):
            item = get_object_or_404(Item, id=self.kwargs['item_pk'])
            if item.task.author == self.request.user and not item.task.is_completed:
                return True
            return False
    
        def get_context_data(self, **kwargs):
            context = super(DeleteItemView, self).get_context_data(**kwargs)
            context['task'] = Task.objects.filter(id=self.kwargs['task_pk'])[0]
            return context
    

    In fact one thing to note would be that if the Task is the one that is related to the Item you don't even need to pass it in your url, you can simply write:

    task = item.task