class Tool(models.Model):
owner = models.ForeignKey("auth.User", on_delete=models.CASCADE, related_name="tools_listed")
approved_by = models.ForeignKey(
"auth.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="tools_approved"
)
category = models.ForeignKey("Category", on_delete=models.PROTECT, related_name="tools")
tags = models.ManyToManyField("Tag", related_name="tools", blank=True)
name = models.CharField(max_length=100)
class Profile(models.Model):
user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
neighborhood = models.CharField(max_length=100, blank=True)
Loan.borrower uses PROTECT and Tool.owner uses CASCADEmodels.CASCADE # delete this row too, when the related row disappears
models.PROTECT # refuse the delete outright — raises ProtectedError
models.SET_NULL # null out this FK (the field needs null=True)
models.SET_DEFAULT # fall back to the field's default value
Picking CASCADE for every foreign key without a second thought is the easy path, and Priya nearly took it on Loan.borrower before realizing what it would actually do. CASCADE genuinely is the right call for Tool.owner — if a member deletes their ToolShed account, it makes sense for the tools they listed to go with them; nobody else has any claim on a drill that was never theirs. It is a genuinely bad default for Loan.borrower: with CASCADE there, the moment that same member deletes their account, every Loan row where they're the borrower disappears — including the still-open record that they currently have someone else's ladder and haven't returned it yet. PROTECT refuses that delete outright and raises ProtectedError instead — verified directly — which forces whatever's deleting the account to deal with open loans first, rather than quietly losing the one record that says where the ladder actually is.
Tool.approved_by — whichever moderator signed off on a listing — gets SET_NULL instead of either: the tool listing itself should survive a moderator's account being deleted, it just no longer needs to remember who approved it. SET_NULL requires null=True on the field itself, which is easy to forget and produces an immediate, unambiguous error at migration time if you do.
related_name, and the clash Django won't let you shipEvery ForeignKey quietly creates a way to query backwards, from the model it points at — accessible as <lowercase model>_set unless a related_name is supplied, in which case that name replaces it entirely:
priya = User.objects.get(username="priya")
priya.tools_listed.all() # every Tool Priya owns, via related_name="tools_listed"
# without related_name, this would default to priya.tool_set.all()
Tool has two separate foreign keys pointing at User — owner and approved_by. Leave both without an explicit related_name and both would default to the identical reverse accessor, user.tool_set, and Django refuses to even start the app over it — verified directly, this is check fields.E304: "Reverse accessor 'User.tool_set' for 'catalog.Tool.owner' clashes with reverse accessor for 'catalog.Tool.approved_by'." That's the concrete reason related_name shows up explicitly on real foreign keys constantly, rather than being left at its default the way a single, unambiguous FK usually can be.