Restricted ManyToMany relationships in Django - django

I'm hoping someone can point me to the most Djangoic way to represent the following general relationship with models, so that Django's existing logic naturally enforces the relationship.
Thing A and Thing B both have one of a number of Types. Thing A can be related to many, one, or no Things Bs and vice-versa (in a symmetric fashion), however Thing A and Thing B can be related if and only if they share the same Type.
My current implementation is to have three models, A, B, and Type, where As and Bs have a foreign key to Type, and A has a m2m with B.
class A(models.Model):
b = models.ForeignKey(B)
typ = models.ManyToManyField(Type)
class B(models.Model):
a = models.ForeignKey(A)
class Type(models.Model):
name = models.CharField()
This lets me do what I want, but doesn't enforce the fact that A can't have a B of another Type. I can use filtering logic in views I control, but where I have less control, like in the Admin, Django lets me map As to Bs of different Types. Is there another way to represent the relationship between As, Bs, and Types in Django?

This would be my approach off the top of my head:
class A(models.Model):
typ = models.ManyToManyField(Type)
#property
def related2B(self)
return list of relationships
def save
check that relationship is still valid if typ field changes
class B(models.Model):
typ = models.ManyToManyField(Type)
#property
def related2A(self)
return list of relationships
def save
check that relationship is still valid if typ field changes
class Type(models.Model):
name = models.CharField()
class Relationship(models.Model):
classA = models.ForeignKey(A)
classB = models.ForeignKey(B)
def save
do check that they share a type before saving

Have you looked at limit_choices_to as a way to control the scope of the M2M?
It can take Q objects for complex queries, but am guessing not F ones (for complex queries including the parent object's current state), but you still might be able to make it deny the save if inappropriate

Related

Django - edit both sides of a many-to-many relation with generic UpdateView

I have a question whether or not it is possible to use the generic UpdateView class to edit "both sides" of a many-to-many relationship.
I have the following classes defined in models.py:
class SomeCategory(models.Model):
code = models.CharField(max_length=5)
name = models.CharField(max_length=40)
class SomeClass(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=False)
age = models.IntegerField(null=False)
allowed_categories = models.ManyToManyField(SomeCategory)
These are both dictionary type tables that store sets of configuration data for my application. To allow editing the dictionaries I use simple UpdateViews:
class SomeClassUpdate(UpdateView):
model = SomeClass
template_name = 'admin/edit_class.html'
fields = ['code', 'name', 'age', 'allowed_categories']
ordering = ['code']
This works fine, I get a nice multi-select and everything is perfect. However, I would like to have the possibility to edit the relationship from the side of the SomeCategory table, so I can choose which SomeClass elements are linked to a certain SomeCategory:
class SomeCategoryUpdate(UpdateView):
model = SomeCategory
template_name = 'admin/edit_category.html'
fields = ['code', 'name', ??????? ]
ordering = ['code']
I have tried adding the related_name attribute to the SomeCategory model, but that did not work.
Any ideas if this can be done without using a custom ModelForm?
Key library versions:
Django==1.11.8
psycopg2==2.7.4
PS: this is my very first question asked on stackoverflow, so please let me know if my post is missing any mandatory elements.
Your issue is in the models.py file. You have two classes, but only one of them mentions the other one. You would think that this should be enough since you are using ManyToManyField after all and assume that it would automatically create every connection leading both ways... Unfortunately this is not true. On the database level it does indeed create a separate intermediary table with references to objects in both original tables, but that doesn't mean that both of them will be automatically visible in Django Admin or similar.
If you would attempt to simply create another someclass = models.ManyToManyField(SomeClass) in the SomeCategory class that would fail. Django would try to create another separate intermediary table through which the connection between two main tables is established. But because the name of the intermediary table depends on where you define the ManyToManyField connection, the second table would be created with a different name and everything would just logically collapse (two tables having two separate default ways to have a ManyToMany connection makes no sense).
The solution is to add a ManyToManyField connection to SomeCategory while also referencing that intermediary/through table that was originally created in the SomeClass class.
A couple of notes about Django/python/naming/programming conventions:
Use the name of the table you are referencing to, as the name of the field that is containing the info about that connection. Meaning that SomeClass's field with a link to SomeCategory should be named somecategory instead of allowed_categories.
If the connection is one-to-many - use singular form; if the connection is many-to-many - use plural. Meaning that in this case we should use plural and use somecategories instead of somecategory.
Django can automatically pluralize names, but it does it badly - it simply adds s letter to the end. Mouse -> Mouses, Category -> Categorys. In those kind of cases you have to help it by defining the verbose_name_plural in the special Meta class.
Using references to other classes without extra 's works only if the the class was already defined previously in the code. In the case of two classes referring to each other that is true only one way. The solution is to put the name of the referred class in the quotation marks like 'SomeCategory' instead of SomeCategory. This sort of reference, called a lazy relationship, can be useful when resolving circular import dependencies between two applications. And since by default it's better to keep the style the same and to avoid unnecessary brain energy wasting of "I will decide whether or not to use quotation marks depending on the order the classes have been organized; I will have to redo this quotation marks thingie every time I decide to move some code pieces around" I recommend that you simply use quotation marks every time. Just like when learning to drive a car - it's better to learn to always use turn signals instead of first looking around and making a separate decision of whether someone would benefit from that information.
"Stringifying" (lazy loading) model/class/table name is easy - just add 's around. You would think that stringifying the "through" table reference would work the same easy way. And you would be wrong - it will give you the ValueError: Invalid model reference. String model references must be of the form 'app_label.ModelName'. error. In order to reference the stringified "through" table you need to: (a) add 's around; (b) replace all dots (.) with underscores (_); (c) delete the reference to through!.. So SomeClass.somecategories.through becomes 'SomeClass_somecategories'.
Therefore the solution is this:
class SomeCategory(models.Model):
code = models.CharField(max_length=5)
name = models.CharField(max_length=40)
someclasses = models.ManyToManyField('SomeClass', through='SomeClass_somecategories', blank=True)
class Meta:
verbose_name_plural = 'SomeCategories'
class SomeClass(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=False)
age = models.IntegerField(null=False)
somecategories = models.ManyToManyField('SomeCategory')
After this it should be obvious what kind of final changes to make to your UpdateView classes.
You can achieve this in the view and form, without having to specify the additional ManytoMany connections in the
models, using something like the following:
In the View
class SomeClassUpdate(UpdateView):
model = SomeClass
form_class = SomeClassUpdateForm # to specify the form
template_name = 'admin/edit_class.html'
def form_valid(self, form, *args, **kwargs):
initial_somecategorys = SomeCategory.objects.filter(allowed_categories__pk=form.instance.pk)
amended_somecategorys = form.cleaned_data['allowed_categroies']
remove = [x for x in initial_somecategorys if x not in amended_somecategorys]
add = [x for x in amended_somecategorys if x not in initial_somecategorys]
for somecategory in add:
somecategory.allowed_categories.add(form.instance)
somecategory.save()
for somecategory in remove:
somecategory.allowed_categories.remove(form.instance)
somecategory.save()
return super().form_valid(form)
In the Form
The init method at the top pre-populates the form with entries saved on the model.
class SomeClassUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SomeClassUpdateForm, self).__init__(*args, **kwargs)
try:
obj = kwargs['instance']
self.fields["some_categories"].initial = SomeCategory.objects.filter(allowed_categories__pk=form.instance.pk)
except (AttributeError, KeyError): # to catch NoneType if new entry being created.
pass
some_categories = forms.ModelMultipleChoiceField(
required=False,
queryset=SomeCategory.objects.all(),
)
class Meta:
model = SomeClass
fields = [
'some_categories'
..etc
]
This should work. I've writen similar code in one of my projects, and it's working fine. However, I don't know if it's
structurally best to use methods like this and not alter the model relationships or whether it's preferable to
alter the model relationships as outlined in other replies. So I'd be interested to know other peoples views on what
the best approach is.

Can I define fields on an implicit many-to-many relationship?

Here's a version of my models.py file. I've removed irrelevant fields and the model names are made up, for security reasons:
class FilmStudio(models.Model):
name = models.CharField(max_length=200, unique=True)
class ProductionCompany(models.Model):
name = models.CharField(max_length=200)
film_studio = models.ForeignKey(FilmStudio)
class Meta:
# Ensure that a given combination of ProductionCompany name and FilmStudio object is unique
unique_together = ('name', 'film_studio')
class Film(models.Model):
title = models.CharField(max_length=200)
production_company = models.ForeignKey(ProductionCompany)
class Actor(models.Model):
name = models.CharField(max_length=200)
films = models.ManyToManyField(Film, blank=True)
Although it is not explicitly defined, there is a many-to-many relationship between an Actor and a FilmStudio. This is evidenced by the following call to the Python API:
FilmStudio.objects.filter(productioncompany__film__actor__name='Samuel L. Jackson').distinct()
This returns all of the FilmStudio objects which Samuel L. Jackson is related to, and each one only once. What I'd like is to define extra fields on the relationship between an Actor and a FilmStudio (it doesn't work too well in this example, I know, but it makes sense for my scenario).
Following what is described in Extra fields on many-to-many relationships, I could use an intermediate model to define extra fields on the relationship between a Film and an Actor, for instance.
But this doesn't seem to help me with my problem. I don't want to define the Actor to FilmStudio relationship explicitly, since it's an existing relationship based on other relationships.
Is it possible to define fields on the relationship that I'm describing?
As far as I know, you are not able to do that.
The reason for that is that it is nowhere to store the extra fields of that relationship. If I understand you correctly, these "extra fields" are not implicit in the actor-film or productionstudio-film relationships, so even though you say they are implicit, the extra fields themselves are explicit.
You could try to emulate it by creating an explicit direct relationship whenever it is needed. Then you could simulate the extra fields using the model as an abstraction, but I am not sure if this is what you want. If you opt for this kind of solution you can use default values (in your abstraction) for filling in relationships that don't have a instance yet.
Does that explanation make sense to you?
EDIT:
(I have not double checked that the code works, so be vary)
OK, so you have the original models:
class FilmStudio(models.Model):
name = models.CharField(max_length=200, unique=True)
class ProductionCompany(models.Model):
name = models.CharField(max_length=200)
film_studio = models.ForeignKey(FilmStudio)
class Meta:
# Ensure that a given combination of ProductionCompany name and FilmStudio object is unique
unique_together = ('name', 'film_studio')
class Film(models.Model):
title = models.CharField(max_length=200)
production_company = models.ForeignKey(ProductionCompany)
class Actor(models.Model):
name = models.CharField(max_length=200)
films = models.ManyToManyField(Film, blank=True)
# The "solution" would be:
class ActorProductionComapny(models.Model):
production_company = models.ForeignKey(ProductionCompany, related_name='actors')
actor = models.ForeignKey(Actor, related_name='companies')
# your extra fields here
someproperty = models.CharField(max_length=200)
class Meta:
# let's say one per actor
unique_together = ('production_company', 'actor')
This is going to get messy really quickly
We use a F object like this:
FilmStudio.objects.filter(productioncompany__film__actor__name='Samuel L. Jackson',
productioncompany__film__actor=F('actors__actor'),
actors__someproperty="Plays poker with CEO").distinct()
The tricky part is going to be handling default values (i.e. when there is no value) This would have to be implemented using a custom Manager, but then I am out of my depth.
I will try to explain as well as I can, but it's going to be tricky.
If you want to make a filter on the relationship you may have to do something like this:
def filter_prod(pq_query, someproperty, actor_name):
if someproperty == "Default":
# Great, this means we can ignore the parallel relationship:
return pq_query.filter(productioncompany__film__actor__name=actor_name)
else:
# Here comes the hard part
FilmStudio.objects.filter(productioncompany__film__actor__name=actor_name,
productioncompany__film__actor=F('actors__actor'),
actors__someproperty=someproperty).distinct()
The thing I am trying to illustrate here is that there are two kinds of actor-productioncompany relationships, those with custom field values (non-default), and those without.
Now, you can make a custom getter that looks something like this:
class ProductionCompany(models.Model):
name = models.CharField(max_length=200)
film_studio = models.ForeignKey(FilmStudio)
def get_actors(self):
# This one is not lazy, so be aware
actors = list(self.actors)
# Get a list of actor IDs
actor_ids = [a.actor_id for a in actors]
for actor in Actor.objects.filter(films__production_company_id=self.id):
if actor.id not in actor_ids:
actors.append(ActorProductionComapny(actor=actor, production_company=self)
actor_ids.append(actor.id)
return actors
class Meta:
# Ensure that a given combination of ProductionCompany name and FilmStudio object is unique
unique_together = ('name', 'film_studio')
This should not save the relationship to the database until you call .save() on an instance. You can also add a custom save method that ignores/aports .save() calls where all the values are default. Just remember to check if it is a new instance or not, because you don't want it to cancel a "set back to default" call. You could also make it delete on a "set back to default", but check if you are allowed to do that within .save().
For even more complex queries (mix of default and non-default) you have Q-objects (further down on the page from F objects)
In short, you need to create an extra model to store this extra relational data between Actor and FilmStudio.
class Actor(models.Model):
name = models.CharField(max_length=200)
films = models.ManyToManyField(Film, blank=True)
film_studios = models.ManyToMany(FilmStudio, through='ActorFilmStudio')
class ActorFilmStudio(models.Model):
actor = models.ForeignKey(Actor)
film_studio = models.ForeignKey(FilmStudio)
# define extra data fields here
data1 = models.TextField()
data2 = models.IntegerField()
One way to think about this: the data you're trying to store belongs to an Actor-FilmStudio relation, and is not related in anyway to Film or ProductionCompany.
Your existing ability to retrieve the a set of Actors for a given FilmStudio (or vice-versa) does not necessarily imply you can store relational data belonging to these two models using the models defined in your example.
Keep in mind that each of the models you defined in your example are backed by a table in your database. In the case of Actor.films field, Django creates an extra table to store the many-to-many relationship data.
Since you're looking to store relational data between Actor and FilmStudio, you need to consider where the data will be stored in your database. Can you store the data in the Film model? or the ProductionCompany model?

django models: how to overcome 'through' ManyToMany option limitation

I'm working on a app for allowing users to create and manage user groups by themselfs.
The problem is I want to store which user added a new member to any group.
These are my models at the moment:
class UserManagedGroup(Group):
leader = models.ForeignKey(User, verbose_name=_('group leader'), related_name='leaded_groups')
members = models.ManyToManyField(User, verbose_name=_('members'), through='Membership',
related_name='managed_groups')
class Membership(models.Model):
user = models.ForeignKey(User, related_name='memberships')
group = models.ForeignKey(UserManagedGroup, related_name='memberships')
info = models.OneToOneField('MembershipInfo', verbose_name=_('membership information'))
class Meta:
unique_together = ['user', 'group']
class MembershipInfo(models.Model):
date_added = models.DateField(_('date added'), auto_now_add=True)
made_member_by = models.ForeignKey(User, verbose_name=_('user who made him a member'))
membership_justification = models.TextField(_('membership justification'), blank=True, default='')
#receiver(signals.post_delete, sender=Membership)
def delete_membership_info(sender, instance, *args, **kwargs):
if instance.info.pk:
instance.info.delete()
As you can see, I have a silly MembershipInfo model which would fit much better merged with Membership because of the nature of its fields. Also, MembershipInfos life is bound to its Membership (which is why I had to create this post_delete signal connection).
I can't merge them because of this:
Your intermediate model must contain one - and only one - foreign key to the target model (this would be Person in our example). If you have more than one foreign key, a validation error will be raised.
(In my case I can't use 2 foreign keys to User)
Now, this actually works but, I don't like it. It makes Membership instance creation tedious since I must always create a MembershipInfo instance first. Also, 2 queries instead of 1.
QUESTION Best way of storing 2 foreign keys to the same model (User) bound to my member relationship.
I just worked through a similar problem which included an intermediate model with two foreign keys to the same target. This is what my system looks like:
class Node(models.Model):
receivers = models.ManyToManyField('self', through='Connection', related_name='senders', symmetrical=False)
class Connection(models.Model):
sender = models.ForeignKey(Node, related_name='outgoing')
receiver = models.ForeignKey(Node, related_name='incoming')
I think this illustrates the main requirements for using two foreign keys to the same target in an intermediate model. That is, the model should have a ManyToManyField with the target 'self' (recursive ManyToMany) and the attribute through pointing to the intermediate model. I think it's also necessary that each foreign key be assigned a unique related_name. The symmetrical=False argument applies to recursive relationships if you want them to be one-way, e.g. Node1 sends signals to Node2, but Node2 doesn't necessarily send signals to Node1. It is necessary that the relationship be defined with symmetrical=False in order for a recursive ManyToMany to use a custom 'through' model. If you want to create a symmetrical recursive ManyToMany with a custom 'through' model, advice can be found here.
I found all these interrelationships fairly confusing, so it took me awhile to choose sensible model attributes and related_names that actually capture what the code is doing. To clarify how this works, if I have a node object N, calling N.receivers.all() or N.senders.all() return sets of other Nodes that receive data from N or send data to N, respectively. Calling N.outgoing.all() or N.incoming.all() access the Connection objects themselves, through the related_names. Note that there is still some ambiguity in that senders and receivers could be swapped in the ManyToManyField and the code would work equally well, but the direction becomes reversed. I arrived at the above by checking a test case for whether the 'senders' were actually sending to the 'receivers' or vice versa.
In your case, targeting both foreign keys to User adds a complication since it's not obvious how to add a recursive ManyToManyField to User directly. I think the preferred way to customize the User model is to extend it through a proxy that's connected to User through a OneToOneField. This is maybe unsatisfying in the same way that extending Membership with MembershipInfo is unsatisfying, but it does at least allow you to easily add further customization to the User model.
So for your system, I would try something like this (untested):
class Member(models.Model):
user = models.OneToOneField(User, related_name='member')
recruiters = models.ManyToManyField('self', through = 'Membership', related_name = 'recruits', symmetrical=False)
other_custom_info = ...
class UserManagedGroup(Group):
leader = models.ForeignKey(Member, related_name='leaded_groups')
members = models.ManyToManyField(Member, through='Membership', related_name='managed_groups')
class Membership(models.Model):
member = models.ForeignKey(Member, related_name='memberships')
made_member_by = models.ForeignKey(Member, related_name='recruitments')
group = models.ForeignKey(UserManagedGroup, related_name='memberships')
date_added = ...
membership_justification = ...
The recursive field should be asymmetrical since Member1 recruiting Member2 should not also mean that Member2 recruited Member1. I changed a few of the attributes to more clearly convey the relationships. You can use the proxy Member wherever you would otherwise use User, since you can always access Member.user if you need to get to the user object. If this works as intended, you should be able to do the following with a given Member M:
M.recruiters.all() -> set of other members that have recruited M to groups
M.recruits.all() -> set of other members that M has recruited to groups
M.leaded_groups.all() -> set of groups M leads
M.managed_groups.all() -> set of groups of which M is a member
M.memberships.all() -> set of Membership objects in which M has been recruited
M.recruitments.all() -> set of Membership objects in which M has recruited someone
And for a group G,
G.memberships.all() -> set of Memberships associated with the group
I think this should work and provide a 'cleaner' solution than the separate MembershipInfo model, but it might require some tweaking, for example checking the direction of the recursive field to make sure that recruiters are recruiting recruits and not vice-versa.
Edit: I forgot to link the Member model to the User model. That would be done like this:
def create_member(member, instance, created, **kwargs):
if created:
member, created = Member.objects.get_or_create(user=instance)
post_save.connect(create_member, member=User)
Note that create_member is not a method of Member but is called after Member is defined. By doing this, a Member object should be automatically created whenever a User is created (you may need to set the member fields to null=True and/or blank=True if you want to add users without initializing the Member fields).
The simpliest way that I see is to remove the ManyToMany field from your UserManagedGroup and to merge Membership and MembershipInfo.
You will able to access your members as well with the entry_set fields.

Django Models - Foreign Key of different objects types that share a common base class

I have the following conceptual design in mind for one of my models.
class A(models.Model):
...
class B(A): #Inherits A
fieldA = ...
fieldB = ...
class C(A): #Inherits A
fieldC = ...
fieldD = ...
class D(models.Model):
field = models.ForeignKey(A) #Here lies the problem, should store B or C
Given the models above, I'd like to store a foreign key to either B or C in D but not both.
I tried setting the Meta class property of A to abstract but that doesn't allow a ForeignKey relationship to A. I do not want to ever have an instance of A that isn't B or C, but if necessary, I can restrict this behavior with the save signal.
Is there an easier design that would allow me to store a foreign key from a list of types where all classes inherit from a common base?
I can think of two options:
Use a generic relation in your D class instead of a foreign key.
If you don't need to filter D using specific fields from B or C you could continue with the approach you have now, but add a method to D that would retrieve the child class of field:
class D(models.Model):
field = models.ForeignKey(A)
def get_field(self):
try:
return self.field.b
except B.DoesNotExist:
pass
try:
return self.field.c
except C.DoesNotExist:
pass
This definitely has some performance implications and as you said in your post, you would have to manually ensure that every instance of A has a B or C subclass. Obviously this approach doesn't scale well if you are going to have n number of subclasses.

Django - Append a related field to a queryset

I have two models:
class A(models.Model):
# fields
class B(models.Model):
a = models.ForeignKey(A)
name = models.CharField(max_length=64)
What I want to do is to get a filtered queryset from A in addition to the related objects from B and append them to the queryset from A, so I would be able to access name field this way: A.B.name
Any idea how to do this?
The problem is that, since the relationship is one-to-many, A doesn't have just one B, but rather a b_set
You could so something like:
for b in a.b_set.all():
b.name
But you can't reference just B because that concept doesn't exist. It would however, if you had used a OneToOneField. Then you could easily do:
a.b.name
Because there's only one B for each A. But, you have to model your object after the actual relationships going on, not how you would prefer the api to work.