Django constraint between ManyToManyField and ForeignKey - django

Consider I have following models:
class Cargo(models.Model):
name = models.CharField(default='')
owner = models.ForeignKey(User, on_delete=models.CASCADE)
class Box(models.Model):
name = models.CharField(default='')
owner = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Cargo, blank=True)
I want to avoid situation when I add some cargo object to box with different owner. For example:
cargo = Cargo(owner=1)
box = Box(owner=2)
box.add(cargo)
How to add such a constraint on a model level?

My initial thought is that a great solution to this problem would be to define a custom RelatedManager that overrides the add() method and validates that the user is the same before you actually do the link. However, after searching through the internet for a while, I was unable to find a way to do a custom RelatedManager on the ManyToManyField (docs on RelatedManager).
As a workaround, I would recommend that you create a method on the Box model called addCargo which you use to add cargo. The method could then enforce the validation of the users before adding the cargo. It could look like:
class Box(models.Model):
name = models.CharField(default='')
owner = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Cargo, blank=True)
def addCargo(self, cargo):
if self.owner.id != cargo.owner.id:
raise ValueError("cargo and box must have same user")
self.tags.add(cargo)
And your code to add the cargo would look like:
cargo = Cargo(owner=1)
box = Box(owner=2)
box.addCargo(cargo)
Hope this helps!

Related

how to build query with several manyTomany relationships - Django

I really don't understand all the ways to build the right query.
I have the following models in the code i'm working on. I can't change models.
models/FollowUp:
class FollowUp(BaseModel):
name = models.CharField(max_length=256)
questions = models.ManyToManyField(Question, blank=True, )
models/Survey:
class Survey(BaseModel):
name = models.CharField(max_length=256)
followup = models.ManyToManyField(
FollowUp, blank=True, help_text='questionnaires')
user = models.ManyToManyField(User, blank=True, through='SurveyStatus')
models/SurveyStatus:
class SurveyStatus(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
survey_status = models.CharField(max_length=10,
blank=True,
null=True,
choices=STATUS_SURVEY_CHOICES,
)
models/UserSurvey:
class UserSurvey(BaseModel):
user = models.ForeignKey(User, null=True, blank=True,
on_delete=models.DO_NOTHING)
followups = models.ManyToManyField(FollowUp, blank=True)
surveys = models.ManyToManyField(Survey, blank=True)
questions = models.ManyToManyField(Question, blank=True)
#classmethod
def create(cls, user_id):
user = User.objects.filter(pk=user_id).first()
cu_quest = cls(user=user)
cu_quest.save()
cu_quest._get_all_active_surveys
cu_quest._get_all_followups()
cu_quest._get_all_questions()
return cu_quest
def _get_all_questions(self):
[[self.questions.add(ques) for ques in qstnr.questions.all()]
for qstnr in self.followups.all()]
return
def _get_all_followups(self):
queryset = FollowUp.objects.filter(survey__user=self.user).filter(survey__user__surveystatus_survey_status='active')
# queryset = self._get_all_active_surveys()
[self.followups.add(quest) for quest in queryset]
return
#property
def _get_all_active_surveys(self):
queryset = Survey.objects.filter(user=self.user,
surveystatus__survey_status='active')
[self.surveys.add(quest) for quest in queryset]
return
Now my questions:
my view sends to the create of the UserSurvey model in order to create a questionary.
I need to get all the questions of the followup of the surveys with a survey_status = 'active' for the user (the one who clicks on a button)...
I tried several things:
I wrote the _get_all_active_surveys() function and there I get all the surveys that are with a survey_status = 'active' and then the _get_all_followups() function needs to call it to use the result to build its own one. I have an issue telling me that
a list is not a callable object.
I tried to write directly the right query in _get_all_followups() with
queryset = FollowUp.objects.filter(survey__user=self.user).filter(survey__user__surveystatus_survey_status='active')
but I don't succeed to manage all the M2M relationships. I wrote the query above but issue also
Related Field got invalid lookup: surveystatus_survey_status
i read that a related_name can help to build reverse query but i don't understand why?
it's the first time i see return empty and what it needs to return above. Why this notation?
If you have clear explanations (more than the doc) I will very appreciate.
thanks
Quite a few things to answer here, I've put them into a list:
Your _get_all_active_surveys has the #property decorator but neither of the other two methods do? It isn't actually a property so I would remove it.
You are using a list comprehension to add your queryset objects to the m2m field, this is unnecessary as you don't actually want a list object and can be rewritten as e.g. self.surveys.add(*queryset)
You can comma-separate filter expressions as .filter(expression1, expression2) rather than .filter(expression1).filter(expression2).
You are missing an underscore in surveystatus_survey_status it should be surveystatus__survey_status.
Related name is just another way of reverse-accessing relationships, it doesn't actually change how the relationship exists - by default Django will do something like ModelA.modelb_set.all() - you can do reverse_name="my_model_bs" and then ModelA.my_model_bs.all()

Create object in model having foreigkey relation

I want to create an entry in this Something model in python manage.py shell using this
Someting.objects.create(discussion_title="General", user_username="admin", content="Hello")
models example
class Discussion(models.Model):
title = models.CharField(max_length=255, unique=True, blank=False,)
users = models.ManyToManyField(User, blank=True, )
class Something(models.Model):
user = models.ForeignKey(User,
on_delete=models.CASCADE)
discussion = models.ForeignKey(Discussion, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
content = models.TextField(unique=False, blank=False)
I am getting this error
TypeError: Something() got an unexpected keyword argument 'discussion_title'
First, you have to use double under bar __ to use django's model relation expression.
Someting.objects.get(discussion__title="General", user__username="admin", content="Hello")
Second, you can't use double under bar relation expression when create an object.
if you want to create an object in relation, you have to create in step by step. follow #Nicolas Appriou 's answer
Your Something model does not have a discussion_title field. You need to create a Discussion instance for this.
This model does not have a user_username model either.
discussion = Discussion.objects.create(title="Foobar")
discussion.users.add(User.objects.create(username="Ham")
Something.objects.create(
discussion=discussion,
)

Django model queryset using related names and managers

I have 4 models -
class Group(models.model):
group_id = models.CharField(max_length=10)
name = models.CharField(max_length=20)
class User(models.model):
user_id = models.CharField(max_length=10)
grp = models.ForeignKey(Group, null=True, blank=True)
user_name = models.CharField(max_length=50)
contact_no = models.CharField(max_length=20)
class DesigType(models.model):
desig_name = models.CharField(max_length=10)
class Designation(models.model):
user = models.ForeignKey(User, null=True, blank=True, related_name='desigs')
desig_type = models.ForeignKey(DesigType, null=True, blank=True, related_name='desigs')
Group model holds groups of users. User holds records for each individual user. DesigType has information about "type of designation", like maybe manager, team lead etc. Designation stores the exact designation - for example, for manager DesigType, Designation might have project manager or account manager; similarly, for team lead DesigType, Designation might have front-end lead or back-end lead.
The UI currently shows a list of users under a group. I want to implement a search functionality according to desig_name. The UI sends me the group_id and the text entered by the end-user in the search box, and I have to return only those Users which have the corresponding desig_name.
I have already done the above, by using a property to return a list of desig_names that a User has and checking whether the user input exists in the list.
This is a property under User -
#cached_property
def desig_types(self):
desig_types = []
for value in self.desigs.select_related('desig_type').values('desig_type__desig_name'):
desig_types.append(value['desig_type__desig_name'])
return desig_types
In my view I have a generic search function which takes any user filter to return the appropriate list of users.
group = Groups.objects.get(pk=grp_id)
_queryset = group.user_set.filter(**user_filter)
The above code works for filtering according to contact_no and user_name. For contact_no, **user_filter is group.user_set.filter(contact_no=user_input), and for user_name, it's group.user_set.filter(user_name=user_input). I want it to also work for filtering according to desig_name, but I couldn't figure out how to navigate through the relationships so I wrote the code below, that retrieves a list of user_ids which have the user inputted desig_name.
required_users = []
for user in group.user_set.all():
user_desig_names = user.desig_types
if user_input in user_desig_names:
required_users.append(user.user_id)
return required_users
I then pass in the filter as group.user_set.filter(user_id__in=required_users). But as you see, I have to have an additional code to get the user_ids, instead of directly using **user_filter, like with user_name or contact_no.
Does anyone know how I can do that?

Creating and saving non-empty Django model instance as part of subclass instance

I am experiencing a very strange behavior inconsistent with the Django documentation while creating and saving (inserting into DB) model instance. I've already run out of ideas for possible reason and will be very grateful for any suggestions why Django fails to save all fields in these cases.
This class I am using:
class Person(models.Model):
user = models.ForeignKey(User)
phone_number = models.CharField(max_length=20, blank=True)
address = models.CharField(max_length=200, blank=True)
And here's code that does't work, few cases:
# First Case
new_person = Person()
new_person.user = request.user
new_person.phone_number = '111111'
new_person.save(force_insert=True)
# Second One
new_person = Person(user=request.user, phone_number='111111')
new_person.save(force_insert=True)
# Third One
new_person = Person.objects.create(user=request.user, phone_number='111111')
Basing on official Django docs in any case django should create an object and insert it into DB.
In fact the object is successfully created (and all relevant fields are set), but row inserted into DB has only id and user_id fields filled correctly while phone_number field that was also set, remains blank.
There is, however, no problem to access and update all fields of existing (saved earlier) objects.
Removing blank=True from Person class declaration (with proper table alteration) does't change anything.
EDIT:
Problem turned out to be more sophisticated. Full description and solution in my own answer beneath
Ok, I found an explanation....
It has something to do with inheritance, namely further in the code I wanted to create instance of Person's subclass. So there was another class:
class Person(models.Model):
user = models.ForeignKey(User)
phone_number = models.CharField(max_length=20, blank=True)
address = models.CharField(max_length=200, blank=True)
class ConnectedPerson(Person):
connection = models.ForeignKey(AnotherClass)
# etc..
And after creating instance of Person, intending to extend it to ConnectedPerson I made such code:
#creating instance of Person:
person = Person(user=request.user, phone_number='111111')
person.save(force_insert=True)
c_person = ConnectedPerson(id=person.id, connection=instance_of_another_c)
and using ConnectedPerson(id=person.id) was in fact killing previously created Person instance by overwritting it in the DB.
So for anyone not too much experienced in managing inheriting instances: if you need to use earlier created super class instance as part of subclass instance do it this way:
#creating person but not saving it
person = Person(user=request.user, phone_number='111111')
######
#later
######
#creating subclass instance and saving
c_person = ConnectedPerson(user=request.user, connection=instance_of_another_c)
c_person.save()
#saving super class instance as part of subclass instance
person.pk = super(ConnectedPerson, c_person).pk
person.save()

How do you set the initial value for a ManyToMany field in django?

I am using a ModelForm to create a form, and I have gotten the initial values set for every field in the form except for the one that is a ManyToMany field.
I understand that I need to give it a list, but I can't get it to work. My code in my view right now is:
userProfile = request.user.get_profile()
employer = userProfile.employer
bar_memberships = userProfile.barmembership.all()
profileForm = ProfileForm(
initial = {'employer': employer, 'barmembership' : bar_memberships})
But that doesn't work. Am I missing something here?
Per request in the comments, here's the relevant parts of my model:
# a class where bar memberships are held and handled.
class BarMembership(models.Model):
barMembershipUUID = models.AutoField("a unique ID for each bar membership",
primary_key=True)
barMembership = USStateField("the two letter state abbreviation of a bar membership")
def __unicode__(self):
return self.get_barMembership_display()
class Meta:
verbose_name = "bar membership"
db_table = "BarMembership"
ordering = ["barMembership"]
And the user profile that's being extended:
# a class to extend the User class with the fields we need.
class UserProfile(models.Model):
userProfileUUID = models.AutoField("a unique ID for each user profile",
primary_key=True)
user = models.ForeignKey(User,
verbose_name="the user this model extends",
unique=True)
employer = models.CharField("the user's employer",
max_length=100,
blank=True)
barmembership = models.ManyToManyField(BarMembership,
verbose_name="the bar memberships held by the user",
blank=True,
null=True)
Hope this helps.
OK, I finally figured this out. Good lord, sometimes the solutions are way too easy.
I need to be doing:
profileForm = ProfileForm(instance = userProfile)
I made that change, and now everything works.
Although the answer by mlissner might work in some cases, I do not think it is what you want. The keyword "instance" is meant for updating an existing record.
Referring to your attempt to use the keyword "initial", just change the line to:
bar_memberships = userProfile.barmembership.all().values_list('pk', flat=True)
I have not tested this with your code, but I use something similar in my code and it works.