How I Fix a Broken Project-Owner Relationship in Django
I was building a project management app in Django where users could own projects and collaborate with others. Simple enough, right? But when I tried to link Account and Project with a many-to-many relationship (so contributors could join projects), Django hit me with cryptic errors like “not a foreign key” and “missing a foreign key”.
Turns out, I misunderstood how through models and through_fields work. Here’s how I fixed it—and how you can avoid my mistakes.
The Original Problem Owners vs. Contributors
My goal was straightforward:
- AÂ
Project should have one owner (anÂAccount). - MultipleÂ
Account users could be contributors to aÂProject.
But my initial code looked like this:
# Broken Model Setup
class Project(models.Model):
project_name = models.CharField(max_length=50)
class Account(models.Model):
username = models.CharField(max_length=50)
projects = models.ManyToManyField(
Project,
through='AccountProjects',
through_fields=('owner', 'project'), # Wait, what’s this??
related_name='contributors'
)
class AccountProjects(models.Model):
owner = models.ForeignKey(Account, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
The Errors I Faced
'AccountProjects.owner' is not a foreign key to 'Project'
I’d incorrectly assumedÂthrough_fields=('owner', 'project') meant “useÂowner fromÂAccount andÂproject fromÂProject“. Nope. Django was yelling: “YourÂowner field points toÂAccount, notÂProject!”Missing foreign key to 'Account' or 'Project'
MyÂAccountProjects model was missing a foreign key to one of the related models. Django requires the intermediate (“through”) model to have FKs to both sides of the M2M relationship.
Simplify and Clarify Relationships
Here’s the corrected code:
# Fixed Models
class Project(models.Model):
project_name = models.CharField(max_length=50)
# Owner is a direct FK (one owner per project)
owner = models.ForeignKey('Account', on_delete=models.CASCADE, related_name='owned_projects')
class Account(models.Model):
username = models.CharField(max_length=50)
# Contributors are linked via M2M through AccountProjects
projects = models.ManyToManyField(
Project,
through='AccountProjects',
related_name='contributors'
)
class AccountProjects(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
Why This Works
through_fields Removed
TheÂthrough_fields parameter is only needed if there’s ambiguity (e.g., multiple FKs to the same model). Here,ÂAccountProjects has one FK toÂAccount and one toÂProject, so Django automatically figures it out.- Explicit Foreign Keys
TheÂAccountProjects model now has bothÂaccount andÂproject FKs, satisfying Django’s requirement for intermediate models. - Separate Owner Field
TheÂowner is defined directly onÂProject, making it clear that ownership is distinct from contributors.
Bonus: Query Owners vs. Contributors
With the fixed setup, you can now:
- List Contributors (Excluding the Owner)
def get_contributors(project):
return Account.objects.filter(accountprojects__project=project).exclude(id=project.owner.id)
- Check if a User is a Contributor
def is_contributor(user, project):
return AccountProjects.objects.filter(account=user, project=project).exists()
- Add Roles to Contributors (e.g., “Editor”, “Viewer”)
class AccountProjects(models.Model):
ROLES = (("viewer", "Viewer"), ("editor", "Editor"))
account = models.ForeignKey(Account, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLES)
Final Thoughts
Here’s what I learned the hard way:
through Models Are Bridges: They need explicit connections to both sides of the relationship.- Ownership ≠Contribution: Separate direct FKs (likeÂ
owner) from M2M fields for clarity. - Django’s Errors Are Clues: They’re frustrating but often point directly to the fix.
Next steps? Run makemigrations and migrate, then test in Django Admin to ensure owners and contributors behave as expected.