Best way to specify one of the existing ManyToMany "through table" relationships?

Viewed 141

I have these models for a database of a library:

class Author(models.Model):
  ...

class Task(models.Model):
  ...  # detailed explanation of how the author collaborated in the book

class BookTask(models.Model):
  book = models.ForeignKey(Book)
  author = models.ForeignKey(Author)
  task = models.ForeignKey(Task)

class Book(models.Model):
  authors = models.ManyToManyField(Author, through='BookTask'...)

Everything there works fine, but I would like to specify one of the existing BookTask relationships as the main one. Think of one book where 3 authors have worked in. I would like to assign all 3 to the book and then set 1 of them as the main one.

I've tried this:

class Book(models.Model):
  authors = models.ManyToManyField(Author, through='BookTask'...)
  author_main = models.ForeignKey(BookTask...)

But then the generated admin webpage doesn't show the expected select choice widget for the author_main field. Any ideas?

(Note: My current solution is adding a boolean field to the BookTask model to specify which one is the main one, and controlling through form validation that one and only one of them for a book is selected. It works, but maybe there is a more elegant solution).

2 Answers

Is the relationship between author/book on one hand and task on the other 1:1? If it is, your solution to add a BooleanField is optimal from database management perspective. Any other foreign key like author_main you have over there will mess up the db schema.

If the relationship is one to many tasks (one author can have many tasks on the same book), I would do this:

class AuthorBook(models.Model):
  book = models.ForeignKey(Book)
  author = models.ForeignKey(Author)
  primary_author = models.BooleanField()

class AuthorBookTask(models.Model):
  author_book = models.ForeignKey(AuthorBook)
  task = models.ForeignKey(Task)

That way you specify authorship first, and then have tasks mapped to that authorship.

Regardless of the above, you would also need some rule specifying that there can be only one primary author per book. The following SQL query should return result less than or equal to 1:

select sum(primary_author)
from AuthorBook
group by book

Hope this helps...

You solution is "elegant" enough in most regards. The part that probably makes you question the solution is having to do validation for relationship data at the admin level, no?

In that case you are correct. There is a simple way to offload this to the data layer and enforce consistency there instead of at the admin validation level.

Using your example you can enforce the data constraint like so:

class BookTask(models.Model):    
    book = models.ForeignKey(Book)
    author = models.ForeignKey(Author)
    task = models.ForeignKey(Task)
    main = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if self.main:
            self.__class__._default_manager.filter(book=self.book, main=True).update(main=False)
        super(BookTask, self).save(*args, **kwargs)

This will basically, "toggle" off main on all other BookTasks for that Book. This will effectively keep only one "main" BookTask for each Book in the database.

Related