What's the best way, in Django, to set and keep up-to-date a many-to-many field that is (for a lack of a better term) a composite of many-to-many fields from other models?
To give a concrete example, I have a Model representing a Resume.
class Resume(models.Model):
owner = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="resume",
)
description = models.TextField(default="Resume description")
skills = models.ManyToManyField(Skill, related_name="resume")
The Resume object is referenced by another model called WorkExperience:
class WorkExperience(models.Model):
...
skills = models.ManyToManyField(Skill, related_name="work")
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=False,
default=1,
related_name="work_experience",
)
resume = models.ForeignKey(
Resume,
on_delete=models.CASCADE,
null=False,
default=1,
related_name="work_experience",
)
Notice that there's a fair amount of redundancy here, with both Resume and WorkExperience pointing to the same owner etc. That said, both of these Models (Resume & WorkExperience) have a field called Skills. That reference another Model called Skills.
What I'd like is to have the Resume skills to point to the same skills as the ones in WorkExperience. Any suggestions on how to do this? I also have a Model called EducationExperience which also references Skills and has a foreign key relation to Resume. Is there any way to keep the skills in Resume be in sync with both the skills in WorkExperience and EducationExperience?
A straightforward option would be to implement a method called set_resume_skills which would, when called, add the skills they have to the list of skills that Resume has
Class WorkExperience(models.Model):
# Same model as above
def set_resume_skills(self):
self.resume.skills.add(self.skills)
The issue I have with this approach is that it only adds skills, it doesn't really keep them in sync. So if I remove a skill from WorkExperience it won't be removed from Resume. Another boundary condition would be that if multiple WorkExperience objects are referencing a skill and then I remove the skill from one Object how would I make sure that the reference in Resume is still intact? I.e., two work experience objects refer to the Skill "Javascript". I remove the Skill from one WorkExperience object. The Skill "Javascript" should still be referenced by the Resume because one WorkExperience object still has a reference to it.
Edit: The reason I want to do it like this is to reduce the amount of querying done on the front-end. If the only way to filter the skills are through the the "sub-models" (WorkExperience, EducationExperience), I'd need to do two queries in my front-end instead of one. Although now that I think about it, doing two queries isn't that bad.
By your design, it seems that Skills actually belong to owner i.e the AUTH_USER. By having its M2M relation in Resume & other models will definitely cause redundancy. Why don't you just create a M2M of Skills in User model?
The point about reducing queries, there are other ways to do this as well. By using select_related or prefetch_related
class User(models.Model):
skills = models.ManyToManyField(Skill, related_name="resume")
# ... Other fields here
class Resume(models.Model):
owner = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="resume",
)
description = models.TextField(default="Resume description")
# ... Other fields here
I was approaching the problem from the wrong end. I don't have to worry about keeping the Skills in check when I just do a reverse query on the Skill objects. Currently I'm filtering the Skill QuerySet in my view like this:
class SkillViewSet(viewsets.ModelViewSet):
serializer_class = SkillSerializer
def get_queryset(self):
queryset = Skill.objects.all()
email = self.request.query_params.get("email", None)
if email:
User = get_user_model()
user = User.objects.get(email=email)
query_work_experience = Q(work__owner=user)
query_education_experience = Q(education__owner=user)
queryset = queryset.filter(
query_work_experience | query_education_experience
)
return queryset
This removes the need to keep the Skills in sync with the Resume Model.
Related
I have two models UserProfile and ChatUser.
ChatUser.models.py
class ChatUser(models.Model):
chat = models.ForeignKey(ChatRoom,on_delete=models.CASCADE)
user = models.ForeignKey(User,on_delete=models.CASCADE)
UserProfile.models.py
class UserProfile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
phone_number = models.IntegerField(default=0)
image = models.ImageField(upload_to='profile_image',blank=True,default='prof1.jpeg')
gender = models.CharField(max_length=10)
joined = JSONField(null=True)
ChatRoom.models
class ChatRoom(models.Model):
eid = models.CharField(max_length=64, unique=True)
name = models.CharField(max_length=100)
location = models.CharField(max_length=50)
vehicle = models.CharField(max_length=50)
brand = models.CharField(max_length=50)
max_limit = models.IntegerField()
joined in UserProfile is an array consisting room ids of the chatrooms model. Now when I delete a ChatRoom row, it automatically deletes the Foreign Key referenced ChatUser object since I am using on_delete=models.CASCADE. But how to update the joined in UserProfile model. I want to remove the id of the deleted ChatRoom from UserProfile.joined
I have used the django.db.models.signals to solve the updating part.
#receiver(post_delete,sender=ChatUser)
def update_profile(sender,instance,**kwargs):
id = instance.chat_id
joined = instance.user.userprofile.joined
if id in joined:
joined.remove(id)
model = profiles.models.UserProfile.objects.filter(user_id=instance.user.id).update(joined=joined)
SDRJ and Willem Van OnSem, thank you for your suggestions
#SAI SANTOSH CHIRAG- Please explain this. You have a ChatUser model that adds user_id and chatroom_id. Now, if I need to find out the list of chatrooms a user has joined, I can simply query this model. If I want to find out the total number of users in a specific chatroom then I can still query this table. Why do I need to keep track of joined in UserProfile? And I am basing this on the premise that joined keeps track of chatroom ids that a user has joined.
At any point, if you choose to add a many-to-many field in any of the models then this is my opinion. E.g Let's assume that you add the following in the UserProfile model
chatroom = models.ManytoManyField(Chat)
Imagine as the number of chatrooms the user joins grows, the list becomes larger and larger and I find it inconvenient because I will have this tiny scroll bar with a large list. It's not wrong but I simply stay away from M2M field for this purpose especially if I expect my list to grow as my application scales.
I prefer the ChatUser approach that you used. Yes, I might have repeating rows of user_ids or repeating chatroom_ids but I don't mind. I can live with it. It's still a bit cleaner to me. And this is simply my opinion. Feel free to disagree.
Lastly, I would rename the ChatUser model to ChatRoomUser...Why? Just by the name of it, I can infer it has something to do with two entities Chatroom and User.
I'm trying to figure out how to store prices between cities in my project so I can work with that comfortabely and admin can change those prices comfortably.
I've decided to create a through model, according to this ANSWER, which is called Ride.
But when I do makemigrations, Django returns:
va_app.City.rides: (fields.E332) Many-to-many fields with intermediate tables must not be symmetrical.
class City(models.Model):
name = models.CharField(max_length=80)
country = models.ForeignKey('Country')
_close_cities = models.ManyToManyField('City', blank=True, related_name='close_cities_set',symmetrical=True)
rides = models.ManyToManyField('self',through='Ride')
class Ride(models.Model):
price = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
Do you know how to make it work?
PS> The only thing I want is to be able to simple access the price (like City.price(City) or something else and admin to be able to change prices.
The error is pretty clear you can't have M2M relation with intermediate table and symmetrical=True, it must be symmetrical=False.
So try with:
rides = models.ManyToManyField('self', through='Ride', symmetrical=False)
However, I think something is wrong with your model structure, you have two M2M fields pointing to self? I'm not sure whats the purpose of the Rides model, but maybe this model should only have FKs to city.
I have a semi complex model relationship in my django app.
I have a Group (a business) which can have many locations
So
A group can have many providers (person) as well. The thing is, the Provider is connected to a particular group through the group location. That is to say a provider can have that location. In reality, i believe a provider can have many locations (belonging to multiple groups). The way I have this in my django so far which I don't think correct is this way:
class GroupLocations(models.Model):
address_id = models.ForeignKey(Address, on_delete= models.SET_NULL)
group_id = models.ForeignKey(Group, on_delete=models.SET_NULL)
doing_business_as = models.CharField(max_length = 255)
created_at=models.DateField(auto_now=false, auto_now_add=true)
updated_at=models.DateField(auto_now=true, auto_now_add=true)
class ProviderLocations(models.Model):
provider_id = models.ForeignKey(Provider, on_delete=models.CASCADE)
group_location_id = models.ForeignKey(GroupLocations, on_delete=models.CASCADE)
created_at=models.DateField(auto_now=false, auto_now_add=true)
updated_at=models.DateField(auto_now=true, auto_now_add=true)
My question is, does my Group (and/or Provider) model need to have some sort of relationship specified in their model definitions?
class Group(models.Model):
group_name = models.CharField(max_length=50)
group_contact= models.CharField(max_length=50)
#do I need something like the following:
providers = models.ManyToMany(Provider, through='ProviderLocations')
provider_locations = models.ManyToMany(Group, through='GroupLocations'
class Provider(models.Model):
created_at=models.DateField(auto_now=false, auto_now_add=true)
updated_at=models.DateField(auto_now=true, auto_now_add=true)
groups = models.ManyToManyField(Group, through='GroupLocations')
group_locations = models.ManyToMany(GroupLocations, through='ProviderLocations')
This is so i can get a list of groups from a provider, and the groups locations
and I can get a list of providers from a group and the providers locations.Actually more like the locations join which to which. I am still learning Django'ss relationship systems so any constructive criticism of how to make these relationships work well together would be helpful.
My question is, does my Group (and/or Provider) model need to have
some sort of relationship specified in their model definitions?
Yes a many to many relationship. And you only need to define it for one model or the other because many to many can be traversed in both direction.
Both ends of a many-to-many relationship get automatic API access to
the other end. The API works just as a “backward” one-to-many
relationship, above.
The only difference is in the attribute naming: The model that defines
the ManyToManyField uses the attribute name of that field itself,
whereas the “reverse” model uses the lowercased model name of the
original model, plus '_set' (just like reverse one-to-many
relationships).
Source: https://docs.djangoproject.com/en/dev/topics/db/queries/#many-to-many-relationships
Thus your models can be simplified.
class Group(models.Model):
group_name = models.CharField(max_length=50)
group_contact= models.CharField(max_length=50)
providers = models.ManyToMany(Provider, through='ProviderLocations')
class Provider(models.Model):
created_at=models.DateField(auto_now=false, auto_now_add=true)
updated_at=models.DateField(auto_now=true, auto_now_add=true)
I am not quite sure why you are trying to create both a GroupLocation and ProviderLocation model. I do believe they can be merged.
I'm working on a maintenance project which has a model say Business with a custom Model Manager. This custom Model Manager adds some extra filter to all the queries executing on Business models. This Business model has a ManyToMany field to self named Trainers. So far so good, the issue comes in when I try to fetch all the Trainers associated with the Business without applying those filters.
The Business model is as given below:
class Business(Basetable):
#status P=publish H=inactive D=draft N=new
name = models.CharField(max_length=120)
slug = models.SlugField(max_length=150)
logo=models.OneToOneField("BusinessLogo",null=True,on_delete=models.SET_NULL)
categories = models.ManyToManyField("BusinessCategory",related_name='allcategories',null=True)
price_type = models.CharField(max_length=2,
choices=PRICE_CHOICES,
default=YEARLY, null=True, blank=True)
achievements = models.TextField(null=True, blank=True)
years_of_experience = models.FloatField(null=True, blank=True)
trainers = models.ManyToManyField("self",related_name='btrainers',null=True, blank=True, symmetrical=False)
expense=models.IntegerField(null=True,blank=True)
objects= CityManager()
def get_trainers(self):
return self.trainers.all()
get_trainers is the function which returns all the Trainers associated with the Business, however I want the results to bypass the CityManager and use the default Manager.
Any pointers will be appreciated.
Update:
using use_for_related_fields = False does not work. I found a related bug here. Is there a work around? I know that overriding the default objects is not a good practice, however this is what I have received.
In general, it's better to avoid filtering results in the default Manager:
It's a good idea to be careful in your choice of default manager in order to avoid a situation where overriding get_queryset() results in an inability to retrieve objects you'd like to work with.
But if you can't change the default Manager for backwards-compatibility reasons, you can still explicitly create a plain Manager and get your results using that.
class Business(Basetable):
...
objects = CityManager() # Still the first listed, and default
plain_objects = models.Manager()
Now that you have a plain Manager, use it to explicitly access the desired objects:
def get_trainers(self):
return Business.plain_objects.filter(btrainers__id=self.id)
I am creating a web application to manage robotics teams for our area. In the application I have a django model that looks like this:
class TeamFormNote(models.Model):
team = models.ForeignKey(Team, blank=True, null=True)
member = models.ForeignKey(TeamMember, blank=True, null=True)
notes = models.TextField()
def __unicode__(self):
if self.team:
return "Team Form Record: " + unicode(self.team)
if self.member:
return "Member Form Record: " + unicode(self.member)
Essentially, I want it to have a relationship with team or a relationship with member, but not both. Is there a way to enforce this?
I can only see two viable solutions. First is actually the same as #mariodev suggested in the comment which is to use Genetic foreign key. That will look something like:
# make sure to change the app name
ALLOWED_RELATIONSHIPS = models.Q(app_label = 'app_name', model = 'team') | models.Q(app_label = 'app_name', model = 'teammember')
class TeamFormNote(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to=ALLOWED_RELATIONSHIPS)
relation_id = models.PositiveIntegerField()
relation = generic.GenericForeignKey('content_type', 'relation_id')
What that does is it sets up a generic foreign key which will allow you to link to any other model within your project. Since it can link to any other model, to restrict it to only the models you need, I use the limit_choices_to parameter of the ForeignKey. This will solve your problem since there is only one generic foreign key hence there is no way multiple relationships will be created. The disadvantage is that you cannot easily apply joins to generic foreign keys so you will not be able to do things like:
Team.objects.filter(teamformnote_set__notes__contains='foo')
The second approach is to leave the model as it and manually go into the database backend and add a db constaint. For example in postgres:
ALTER TABLE foo ADD CONSTRAINT bar CHECK ...;
This will work however it will not be transparent to your code.
This sounds like a malformed object model under the hood...
How about an abstract class which defines all common elements and two dreived classes, one for team and one for member?
If you are running into trouble with this because you want to have both referenced in the same table, you can use Generic Relations.