I need to express multiple one-to-many relations in Django. That is, given several different models, I need each of these to have a one-to-many relation with a single table. Logically, the relation belongs to the model "owning" the one-to-many relation, but Django forces me to use a many-to-one relation on the target table, instead of a one-to-many relation on the source table. Here's what I wish I could do:
class Sink(models.Model):
name = models.CharField('name', max_length=24)
class A(models.Model):
name = models.CharField('name', max_length=24)
sink = models.ManyToOneField(Sink)
class B(models.Model):
name = models.CharField('name', max_length=24)
sink = models.ManyToOneField(Sink)
but ManyToOneField doesn't exist. Instead, I'm supposed to use ForeignKey for each one-to-many field, like:
class Sink(models.Model):
name = models.CharField('name', max_length=24)
a = models.ForeignKey(A)
b = models.ForeignKey(B)
class A(models.Model):
name = models.CharField('name', max_length=24)
class B(models.Model):
name = models.CharField('name', max_length=24)
which is logically just wrong, since there is never a case where I want both Sink.a and Sink.b to be non-null. If ManyToManyField allowed me to specify that it's not really many-to-many, I could do that, but it doesn't seem to allow that. What's the right way to do something like this?
You could use generic relations to link your Sink model to a single A or B model with a GenericForeignKey:
class Sink(models.Model):
name = models.CharField('name', max_length=24)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class A(models.Model):
name = models.CharField('name', max_length=24)
class B(models.Model):
name = models.CharField('name', max_length=24)
Related
Model "A" is a generic model, meaning any model can relate to it. Model "B" and "C" are models that want to establish a foreign key relation with Model "A". How can this be done?
class A(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
list = # What goes here?
class B(models.Model):
parent = # What goes here to refer to model A?
name = models.CharField(max_length=255)
age = models.IntegerField()
class C(models.Model):
parent = # What goes here to refer to model A?
title = models.CharField(max_length=255)
body = models.TextField()
An example lookup would be:
A.list.all()
Giving me a list of either B objects or C objects.
This is completely the wrong design for what you are asking for. Your structure only allows one single item to be related to each A, whether it is a B or a C.
Instead you need an intermediate model, which contains the GenericForeignKey and which also has a (normal) ForeignKey to A. That is how tagging applications work: see for example django-tagging's TaggedItem model.
I've decided to use different related names which works in my case:
class A(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class B(models.Model):
parent = models.ForeignKey('A', related_name='B')
name = models.CharField(max_length=255)
age = models.IntegerField()
class C(models.Model):
parent = models.ForeignKey('A', related_name='C')
title = models.CharField(max_length=255)
body = models.TextField()
This problem almost drives me crazy :(
I was trying to use the StackedInline in admin interface.
The code below is in django documentation.
model.py
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
admin.py
class MembershipInline(admin.StackedInline):
model = Membership
extra = 1
class PersonAdmin(admin.ModelAdmin):
inlines = (MembershipInline,)
class GroupAdmin(admin.ModelAdmin):
inlines = (MembershipInline,)
But if the Group is an abstract base class and PublicGroup is subclass that inherits from Group. Membership is used to relate PublicGroup and Person.
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='%(class)s_Membership')
class Meta:
abstract = True
class PublicGroup(Group):
pass
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
after running the command
python manage.py sql test
I got error "AssertionError: ForeignKey cannot define a relation with abstract class Group".
After searching for solution, I know foreign key cannot point to a abstract class. Some solutions recommended to use generic relation. So I change the code again.
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = generic.GenericRelation('Membership')
class Meta:
abstract = True
class PublicGroup(Group):
pass
class Membership(models.Model):
person = models.ForeignKey(Person)
content_type = models.ForieignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
This time the command
python manage.py sql test
returns no error. But I got error when I try to add data on admin interface. The error says Membership is not a foreign key of PublicGroup. StackedInline still doesn't work.
Now I really don't know what to do. Does anyone know how to achieve this function.
Thanks for reading!
Is there any good reason why you use this?
class Meta:
abstract = True
If possible, drop it, and then rebuild your database.
It might be interesting for you to read the answers to this stackoverflow question about the difference between abstract models and regular inheritance.
Your model structure is terrible: a M2M relationship will build a relationship table for both Models you try to connect, just like "Through keyword" in M2M field
As I can see, you just want to build a M2M relation between Person and Model based on Group.
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
type = models.CharField(max_lenght=32) # The Type of Group (Public/Private/etc..)
members = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
I have the following models, with publication needing a m2m with authors via the join table specified, I have done this but keep getting the error:
Error: One or more models did not validate:
publications.workshop: 'staff' is a manually-defined m2m relation through model AuthorsJoinTable, which does not have foreign keys to Staff and Workshop
publications.technicalreport: 'staff' is a manually-defined m2m relation through model AuthorsJoinTable, which does not have foreign keys to Staff and TechnicalReport
publications.authorsjointable: 'publication' has a relation with model Publication, which has either not been installed or is abstract.
publications.authorsjointable: "unique_together" refers to staff, a field that doesn't exist. Check your syntax.
My models look like:
class Publication(models.Model):
title = models.CharField(max_length=500)
staff = models.ManyToManyField("personnel.Staff", related_name='%(app_label)s_%(class)s_related', through='AuthorsJoinTable')
tag = models.ManyToManyField("Tag", related_name='%(app_label)s_%(class)s_related')
class Meta:
abstract = True
class Workshop(Publication):
location = models.CharField(max_length=100)
workshop_title = models.CharField(max_length=100)
start_date = models.DateField()
end_date = models.DateField()
def __unicode__(self):
return u'%s - %s' % (self.title, self.workshoptitle)
class TechnicalReport(Publication):
published_date = models.DateField()
class AuthorsJoinTable(models.Model):
author = models.ForeignKey("Author", related_name='%(app_label)s_%(class)s_from')
publication = models.ForeignKey("Publication", related_name='%(app_label)s_%(class)s_to')
order = models.IntegerField()
class Meta:
unique_together = ('staff', 'publication')
class Tag(models.Model):
tag_name = models.CharField(max_length=100, primary_key=True)
class Author(models.Model):
name = models.CharField(max_length=100)
biography = models.TextField()
So how can I resolve this problem?
publications.authorsjointable: "unique_together" refers to staff, a field that doesn't exist. Check your syntax.
You can't create a ForeignKey on absract model because that model does not have a table in DB and therefore does not have primary key to reference. So you should make your Publication non-abstract or reference Workshop instead. Other error lines should also be gone after that.
I want to have a model with a ManyToMany relationship with itself, I don't know how to write this but I'l try to write some code to illustrate what I want to do.
class Person(models.Model):
name = models.CharField()
occupation = models.CharField()
friends = models.ManyToManyField('self', through = PersonFriends)
My Model that I want the friends to go through
class PersonFriends(models.Model)
???
comment = models.CharField()
In a ManyToMany field with through relationship if the other model's name was "Pet" for example I'd name my fields in that through class person and pet and make them models. ForeignKey(Person) and Pet for example
What to I name my fields in my PersonFriends model for the two person-fields now that they are the same model?
You can do something like this:
class Person(models.Model):
name = models.CharField(max_length = 255)
occupation = models.CharField(max_length = 255)
friends = models.ManyToManyField('self', through = 'PersonFriends',
symmetrical = False)
# ^^^^^^^^^^^
# This has to be false when using `through` models. Or else your
# model will not validate.
class PersonFriends(models.Model):
source = models.ForeignKey(Person, related_name = 'source')
# ^^^^^^^^^^^^
# You need different `related_name` for each when you have
# multiple foreign keys to the same table.
target = models.ForeignKey(Person, related_name = 'target')
comment = models.CharField(max_length = 255)
Everything is described in the official docs for ManyToManyField.through_fields (you can search for 'recursive relationships' phrase there to quickly find what you need):
for django 1.11 you have to specify through and (!) through_fields arguments:
class Person(models.Model):
name = models.CharField(max_length=50)
# note the additional arguments here
friends = models.ManyToManyField(
'self',
# recursive relationships to self with intermediary
# through model are always defined as non-symmetrical
symmetrical=False,
through='PersonFriend',
# this argument is required to define a custom
# through model for many to many relationship to self
# position matters: 1 - source (from), 2 - target (to)
through_fields=('person', 'friend'),
)
class PersonFriend(models.Model):
# required relationship-defining foreign keys
# (note that the order does not matter, it matters
# in 'through_fields' argument in 'friends' field of the 'Person' model)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
friend = models.ForeignKey(Person, on_delete=models.CASCADE)
# additional fields
comment = models.CharField()
Without assuming that friendships are symmetrical. Because Buzz Lightyear might be Woody's friend, but Woody isn't friends with Buzz Lightyear till near the end of the film. You can simplify both models and still have reasonable lookup names. You would of course need to make sure that you define two PersonFriends if it's a good friendship.
class Person(models.Model):
name = models.CharField()
occupation = models.CharField()
class PersonFriends(models.Model):
from_person = models.ForeignKey(Person, related_name='friends_with')
to_person = models.ForeignKey(Person, related_name='friends')
comment = models.CharField()
class Meta:
unique_together = ('from_person', 'to_person')
This has the added bonus of a comment for each direction of the friendship. i.e. Tyrion thinks Sansa is a lovely and intelligent, but lost girl. Whereas Sansa might think that Tyrion is an ugly but clever and kind-hearted kinda guy.
class PersonFriends(models.Model):
from_person = models.ForeignKey(Person, related_name='from_person')
to_person = models.ForeignKey(Person, related_name='to_person')
this is from db table structure of a ManyToMany relation to self from my Model structure. Django defines it like that..
Given the following models adapted from http://www.djangoproject.com/documentation/models/generic_relations/
class TaggedItem(models.Model):
"""A tag on an item."""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
class Vegetable(models.Model):
name = models.CharField(max_length=150)
is_yucky = models.BooleanField(default=True)
edible = models.BooleanField(default=True)
class Mineral(models.Model):
name = models.CharField(max_length=150)
hardness = models.PositiveSmallIntegerField()
edible = models.BooleanField(default=True)
How would I filter TaggedItems so that I get only those with content_objects that are edible?
ideally, something like:
TaggedItem.objects.filter(content_object.edible=True)
What if Vegetable and Mineral had is_edible methods?
You can't really do this with generic relations, because there's nothing to guarantee that the target model will even have an edible field.
An alternative is to change the structure to use model inheritance (multi-table). Vegetable and Mineral would both inherit from a Taggable model, which contained the edible field (or anything else you need to filter on). Then TaggedItem would have a standard ForeignKey to Taggable, so you would use the standard double-underscore filter syntax.