Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validation using DeleteView before deleting instance

What's the best approach for handling deletion of an object with some validation before the object is deleted? For example, in my setup have two models - Game and Team (which are obviously related). Users should only be able to delete teams that are NOT tied to any games.

I created a form (without any fields) for deleting a team...

class TeamDeleteForm(ModelForm):
    class Meta:
        model = Team
        fields = []

    def clean(self):
        # Check to see if this team is tied to any existing games
        if self.instance.gameteams_set.exists():
            raise ValidationError("This team is tied to 1 or more games")
        return super().clean()

But then I realized that the class based view DeleteView doesn't have any sort of form_valid() method. Should I extend the generic FormView instead of DeleteView or is there a better approach that I'm missing?

like image 806
Ben Avatar asked Feb 16 '15 02:02

Ben


4 Answers

I think the best approach will be overriding the model's delete method. For example:

class Team(models.Model):
    ...
    def delete(self, *args, **kwargs):
        if Game.objects.filter(team__pk= self.pk).exists():
            raise Exception('This team is related to a game.')  # or you can throw your custom exception here.
        super(Team, self).delete(*args, **kwargs)
like image 126
ruddra Avatar answered Oct 19 '22 04:10

ruddra


For your particular case I would simply override the queryset attribute of your view to exclude Teams with associated Games.

class TeamDeleteView(DeleteView):
    queryset = Team.objects.distinct().exclude(games__isnull=False)

There's a Django ticket opened to make the DeleteView behave like other form views but until the proposed patch is merged and released (It won't make it in 1.8) you'll have to completely override the delete method of your view like the following:

class TeamDeleteView(DeleteView):
    model = Team

    def delete(request, *args, **kwargs):
        self.object = self.get_object()
        if self.object.gameteams_set.exists():
            # Return the appropriate response
        success_url = self.get_success_url()
        self.object.delete()
        return HttpResponseRedirect(success_url)

Edit:

From your accepted solution it looks like you're trying to prevent deletion at the model level. Such enforcement should be done by using a PROTECT on_delete handler.

from django.db import models

class Team(models.Model):
    pass

class Game(models.Model):
    team = models.ForeignKey(Team, on_delete=models.PROTECT)

You'll still have to deal with the raised ProtectedError in your view:

from django.db import models
from django.http.response import HttpResponseForbidden

class TeamDeleteView(DeleteView):
    model = Team

    def delete(request, *args, **kwargs):
        try:
            return super(TeamDeleteView, self).delete(
                request, *args, **kwargs
            )
        except models.ProtectedError as e:
            # Return the appropriate response
            return HttpResponseForbidden(
                "This team is tied to 1 or more games"
            )

You could even use the protected_objects property of e to display a more meaningful error message just like the admin does.

like image 40
Simon Charette Avatar answered Oct 19 '22 04:10

Simon Charette


I've used both DeleteView and FormView for this scenario. Both have their pros and cons.

DeleteView is nice because it's based on the SingleObjectMixin and you can easily get access to the object you want to delete. One nice way of doing this is raising an exception in get_object. This makes it so that you can raise exception on both get and post.

def get_object(self, qs):
  obj = super(FooView, self).get_object(qs)
  if obj.can_delete():
    return obj
  raise PermissionDenied

FormView is nice because you can leverage form_invalid and the clean methods, but then you still have to do the work to get the object, setup some sort of form (not needed in deleteview).

It's really a matter of how you want to tackle it. Some other questions are: do you raise an exception on GET, or do you want to show a nice page letting the user know they can't delete the object. This can be done in both View types.

Update your question if you have more points to go over and i'll update my response.

like image 7
Esteban Avatar answered Oct 19 '22 04:10

Esteban


One other way to do it would be to work with django.db IntegrityError!

from django.db import IntegrityError

class TeamDeleteView(DeleteView):
model = Team

    def delete(self, request, *args, **kwargs):
        """If DB Integrity Error, display msg and redirect to list"""
        try:
            return(super().delete(request, *args, **kwargs))
        except IntegrityError:
            messages.error(request, "This team is tied to 1 or more games")
            return render(request, template_name=self.template_name, context=self.get_context_data())
like image 1
openHBP Avatar answered Oct 19 '22 06:10

openHBP