Edit record before delete. Django DeleteView

Viewed 3283

I have the follow models:

class Category(models.Model):
    ....
    posts = models.PositiveIntegerField(_('numero post'), default=0)
    ....

class Post(models.Model):
    ....
    categories = models.ManyToManyField(Category, blank=True, verbose_name=_('categorie'))
    ....

the Category.posts field counts the number of the posts related to it's own category. I increase the counter into the CreateView in the following way:

class CreatePostView(LoginRequiredMixin, CreateView):
    model = Post
    ...

    def form_valid(self, form, **kwargs):
        ...
        categories = self.object.categories.all()
        for category in categories:
            category.posts = category.posts + 1
            category.save()

        self.object.save()
        return super(CreatePostView, self).form_valid(form)

Whene i delete the blog post i need to decrement the counter when the Post is deleted. I've tried to do in this way:

class DeletePostView(LoginRequiredMixin, DeleteView):
    model = Post
    ...

    def form_valid(self, form, **kwargs):
        ...
        categories = post.categories.all()
        for category in categories:
            if (category.posts - 1) > 0:
                category.posts = category.posts - 1
                category.save()
        return post

But it doesn't work, because the record is deleted before that this method is called.

How can i do this operation before that the record is deleted? Or there is a built-in way to get the number of related records from Category?

2 Answers

There is no form_valid for a DeleteView since a DeleteView has no form.

What we can do is add some logic in the .delete(..) function, which is the function that deletes the object. The DeleteView implements this as:

def delete(self, request, *args, **kwargs):
    """
    Call the delete() method on the fetched object and then redirect to the
    success URL.
    """
    self.object = self.get_object()
    success_url = self.get_success_url()
    self.object.delete()
    return HttpResponseRedirect(success_url)

So we can implement this as:

from django.db.models import F

class DeletePostView(LoginRequiredMixin, DeleteView):

    model = Post
    # ...

    def delete(self, *args, **kwargs):
        self.object = self.get_object()
        self.object.post.categories.all().update(posts=F('posts')-1)
        return super(DeletePostView, self).delete(*args, **kwargs)

The above will update the categories in "bulk" by decrementing the posts field. Here howver, it can reach zero (which is impossible in your given code).

If you want to prevent that, we could use Greatest [Django-doc] to avoid that:

from django.db.models import F, Value
from django.db.models.functions import Greatest

class DeletePostView(LoginRequiredMixin, DeleteView):

    model = Post
    # ...

    def delete(self, *args, **kwargs):
        self.object.post.categories.all().update(posts=Greatest(F('posts')-1, Value(1)))
        return super(DeletePostView, self).delete(*args, **kwargs)

in version 4.0 you'll get the warning DeleteViewCustomDeleteWarning: DeleteView uses FormMixin to handle POST requests. As a consequence, any custom deletion logic in YourDeleteView.delete() handler should be moved to form_valid(). so, go ahead and implement your logic in form_valid

Related