Validate a condition that relies on foreign keys in Django admin

2.8k views Asked by At

I have two models, Book and Page, where Page has a foreign key relation to Book. In admin.py, the webpage to modify a book has inline elements for Page objects.

models.py:

class Book(models.Model):
    # ...

class Page(models.Model):
    book = models.ForeignKey(Book, editable=False)
    number = models.IntegerField('Page number')
    # ...

admin.py:

class PageAdminInline(admin.TabularInline):
    model = Page
    extra = 1

class BookAdmin(admin.ModelAdmin):
    inlines = [PageAdminInline]

admin.site.register(Book, BookAdmin)

In the inline forms, a field allows to set the page number for each page. With this configuration, how can I validate the global condition that all pages numbers of a book should be different and numbered from one to the number of Page objects associated to the book?

I suppose I have to override a clean() method related to a book model or form somewhere, but I don't know how to access from there the data related to the pages that the user inputs.

Edit

Based on Emett's suggestion, I have tried to override the clean() method of the Page model:

class Page(models.Model):
    book = models.ForeignKey(Book, editable=False)
    number = models.IntegerField('Page number')

    def clean():
        book_pages = Page.objects.filter(book=self.book)
        # ... [apply condition on the book_pages QuerySet]
        super(Page, self).clean()

This does not work: if I modify the page number of all pages in the admin site for a book, book_pages will contain objects that have the old page numbers.

In addition, having the condition checked in Page also means that it will be tested n times if I have n pages, while just checking it once should be sufficient.

1

There are 1 answers

7
ruddra On

An easier solution would be putting unique_together in django model. IE

class Page(models.Model):
    book = models.ForeignKey(Book, editable=False)
    number = models.IntegerField('Page number')

    class Meta:
        app_label = 'page'
        db_table = 'pages'
        verbose_name = 'Page'
        verbose_name_plural = 'Pages'
        unique_together = (('book', 'number'),)

Another workaround if you don't want to use unique_together, then create a form, use it in inline, ie:

class PageForm(forms.ModelForm):
   class Meta:
     model = Page
     fields ='__all__'

   def clean():
       cleaned_data = self.cleaned_data
       book = cleaned_data.get('book')
       number = cleaned_data.get('number')
       page_qset = Page.objects.filter(book=book, number=number)

       if len(page_qset) > 0:  # inefficient solution, using it for forcibly executing query
             raise forms.ValidationError('Already exists')
       return super().clean()



class PageAdminInline(admin.TabularInline):
    form = PageForm