Flimm
Flimm

Reputation: 150613

How can make the admin for a ForeignKey('self') ban referring to itself?

I have a model with a forgein key to itself. For example:

class Folder(models.Model):
    name = models.CharField()
    parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)

For my purposes, I never want parent_folder to refer to itself, but the default admin interface for this model does allow the user to choose its own instance. How can I stop that from happening?

Edit: If you're trying to do a hierarchical tree layout, like I was, another thing you need to watch out for is circular parent relationships. (For example, A's parent is B, B's parent is C, and C's parent is A.) Avoiding that is not part of this question, but I thought I would mention it as a tip.

Upvotes: 2

Views: 1575

Answers (2)

Flimm
Flimm

Reputation: 150613

To make sure the user does not select the same instance when filling in the foreign key field, implement a clean_FIELDNAME method in the admin form that rejects that bad value.

In this example, the model is Folder and the foreign key is parent_folder:

from django import forms
from django.contrib import admin
from .models import Folder

class FolderAdminForm(forms.ModelForm):
    def clean_parent_folder(self):
        if self.cleaned_data["parent_folder"] is None:
            return None
        if self.cleaned_data["parent_folder"].id == self.instance.id:
            raise forms.ValidationError("Invalid parent folder, cannot be itself", code="invalid_parent_folder")
        return self.cleaned_data["parent_folder"]

class FolderAdmin(admin.ModelAdmin):
    form = FolderAdminForm

admin.site.register(Folder, FolderAdmin)

Edit: Combine my answer with raphv's answer for maximum effectiveness.

Upvotes: 1

raphv
raphv

Reputation: 1183

I would personally do it at the model level, so if you reuse the model in another form, you would get an error as well:

class Folder(models.Model):
    name = models.CharField()
    parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)

    def clean(self):
        if self.parent_folder == self:
            raise ValidationError("A folder can't be its own parent")

If you use this model in a form, use a queryset so the model itself doesn't appear:

class FolderForm(forms.ModelForm):

    class Meta:
        model = Folder
        fields = ('name','parent_folder')

    def __init__(self, *args, **kwargs):
        super(FolderForm, self).__init__(*args, **kwargs)
        if hasattr(self, 'instance') and hasattr(self.instance, 'id'):
            self.fields['parent_folder'].queryset = Folder.objects.exclude(id=self.instance.id)

Upvotes: 4

Related Questions