How to repeat a many-to-many relationship in Django? - python

I have these models in my Django app:
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Animal(models.Model):
name = models.CharField(max_length=100, unique=True)
class AnimalList(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
list = models.ManyToManyField(Amimal)
I want to add the same pokemon to the same list twice:
>>> animal = Animal.objects.create(name='milla')
>>> user = User.objects.create(username='user')
>>> list = AnimalList.objects.create(user=user)
>>> list.list.add(animal)
>>> list.list.add(animal)
>>> animal.save()
The animal is added only once, though:
>>> list.list.all()
<QuerySet [<Animal: Animal object (3)>]>
I expected that, the documentation is explicit that
Adding a second time is OK, it will not duplicate the relation:
Yet, I do want to repeat the animals.
How could I do that? Is it possible by using ManyToManyField?

I could not repeat these relationships because Django applies a UniqueConstrant to many-to-many relationships. My solution was to add another table that referenced the animals:
class AnimalListItem(models.Model):
animal = models.ForeignKey(Animal, on_delete=models.CASCADE)
class AnimalList(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
list_items1 = models.ManyToManyField(AnimalListItem)
After that, every time I wanted to add an animal to a list, I had to create a list item first, pointing to the animal, then add this list item to the list column.
There are other possible solutions as well. For example, the through argument from ManyToManyField disables any default constraints that would be added to the relationship table. Or you could set db_constraint to False without through.
However, those were not solutions to me because, as the documentation states:
If the custom through table defined by the intermediate model does not enforce uniqueness on the (model1, model2) pair, allowing multiple values, the remove() call will remove all intermediate model instances
I needed to remove only one instance at a time, so removing all of them would not be feasible.

Related

Deleting auto created model which is under PROTECTED ForeignKey

class Basket:
name = models.CharField(max_length=50, blank=True, null=True)
class Apple:
name = models.CharField(max_length=50, blank=True, null=True)
basket = models.ForeignKey(Basket, on_delete=models.PROTECT)
...
myapple = new Apple(name="my")
myapple.save()
...
auto_created_basket = myapple.basket
myapple.basket = existing_basket
auto_created_basket.delete()
I try to swap out the auto_created_basket to another one, but I get an error when I try to delete it.
"Cannot delete some instances of model 'Basket' because they are referenced through a protected foreign key: 'Apple.basket'", [<Apple: My apple>])
In your Apple model the basket field is a foreing key
basket = models.ForeignKey(Basket, on_delete=models.PROTECT)
whose on_delete attribute value specifically states to protect the apples by preventing the deletion of the basket.
As the official docs say
When an object referenced by a ForeignKey is deleted, Django by
default emulates the behavior of the SQL constraint ON DELETE CASCADE
and also deletes the object containing the ForeignKey.
and
the PROTECT parameter prevents deletion of the referenced object by raising
ProtectedError
So, the easiest step should be to remove the on_delete parameter and go with the default behaviour
basket = models.ForeignKey(Basket)
However, please have a look at all the possible parameters to a ForeignKey model field and choose the combination that suits the requirements of your application/scenario.
UPDATE:
Recent Django versions require on_delete. Just don't remove it and add the parameter you want (like on_delete=models.CASCADE).
I hate to answer my question, but my example was too over simplified. The answers are very good points.
In the real product there are post_save signals involved, responsible for the creation of the auto_created_basket for example.
The problem is that when I say myapple.basket = existing_basket Django's layer is fine, but the DB is still holding the reference to older relation. The solution in my case was to move the auto_created_basket.delete() after I save myapple once more.
You can try moving apples from auto_created_basket into existing_basket before deleting the basket first:
>>> auto_created_basket.apple_set.update(basket=existing_basket)
>>> auto_created_basket.delete()
or
>>> myapple.basket = existing_basket
>>> myapple.save()
>>> auto_created_basket.delete()
Alternatively, if you want to collect apples that are from deleted baskets in a single basket, you can assign a function to on_delete property as follows:
def get_sentinel_basket():
basket, created = Basket.objects.get_or_create(name='DELETED')
return basket
--
basket = models.ForeignKey(Basket, on_delete=models.SET(get_sentinel_basket))
so when a basket is deleted, the .basket attribute of apples in that basket will automatically be set to Basket(name='DELETED').

How to use unique_together method in django views

class Model1(models.Model):
username = models.CharField(max_length=100,null=False,blank=False,unique=True)
password = models.CharField(max_length=100,null=False,blank=False)
class Model2(models.Model):
name = models.ForeignKey(Model1, null=True)
unique_str = models.CharField(max_length=50,null=False,blank=False,unique=True)
city = models.CharField(max_length=100,null=False,blank=False)
class Meta:
unique_together = (('name', 'unique_str'),)
I've already filled 3 sample username-password in Model1 through django-admin page
In my views I'm getting this list as
userlist = Model1.objects.all()
#print userlist[0].username, userlist[0].password
for user in userlist:
#here I want to get or create model2 object by uniqueness defined in meta class.
#I mean unique_str can belong to multiple user so I'm making name and str together as a unique key but I dont know how to use it here with get_or_create method.
#right now (without using unique_together) I'm doing this (but I dont know if this by default include unique_together functionality )
a,b = Model2.objects.get_or_create(unique_str='f3h6y67')
a.name = user
a.city = "acity"
a.save()
What I think you're saying is that your logical key is a combination of name and unique_together, and that you what to use that as the basis for calls to get_or_create().
First, understand the unique_together creates a database constraint. There's no way to use it, and Django doesn't do anything special with this information.
Also, at this time Django cannot use composite natural primary keys, so your models by default will have an auto-incrementing integer primary key. But you can still use name and unique_str as a key.
Looking at your code, it seems you want to do this:
a, _ = Model2.objects.get_or_create(unique_str='f3h6y67',
name=user.username)
a.city = 'acity'
a.save()
On Django 1.7 you can use update_or_create():
a, _ = Model2.objects.update_or_create(unique_str='f3h6y67',
name=user.username,
defaults={'city': 'acity'})
In either case, the key point is that the keyword arguments to _or_create are used for looking up the object, and defaults is used to provide additional data in the case of a create or update. See the documentation.
In sum, to "use" the unique_together constraint you simply use the two fields together whenever you want to uniquely specify an instance.

Is there a way to simulate a foreach in Django using queries?

I have a model 'Status' with a ManyToManyField 'groups'. Each group has a ManyToManyField 'users'. I want to get all the users for a certain status. I know I can do a for loop on the groups and add all the users to a list. But the users in the groups can overlap so I have to check to see if the user is already in the group. Is there a more efficient way to do this using queries?
edit: The status has a list of groups. Each group has a list of users. I want to get the list of users from all the groups for one status.
Models
class Status(geomodels.Model):
class Meta:
ordering = ['-date']
def __unicode__(self):
username = self.user.user.username
return "{0} - {1}".format(username, self.text)
user = geomodels.ForeignKey(UserProfile, related_name='statuses')
date = geomodels.DateTimeField(auto_now=True, db_index=True)
groups = geomodels.ManyToManyField(Group, related_name='receivedStatuses', null=True, blank=True)
class Group(models.Model):
def __unicode__(self):
return self.name + " - " + self.user.user.username
name = models.CharField(max_length=64, db_index=True)
members = models.ManyToManyField(UserProfile, related_name='groupsIn')
user = models.ForeignKey(UserProfile, related_name='groups')
I ended up creating a list of the groups I was looking for and then querying all users that were in any of those groups. This should be pretty efficient as I'm only using one query.
statusGroups = []
for group in status.groups.all():
statusGroups.append(group)
users = UserProfile.objects.filter(groupsIn__in=statusGroups)
As you haven't posted your models, its a bit difficult to give you a django queryset answer, but you can solve your overlapping problem by adding your users to a set which doesn't allow duplicates. For example:
from collections import defaultdict
users_by_status = defaultdict(set)
for i in Status.objects.all():
for group in i.group_set.all():
users_by_status[i].add(group.user.pk)
Based on your posted model code, the query for a given status is:
UserProfile.objects.filter(groupsIn__receivedStatuses=some_status).distinct()
I'm not 100% sure that the distinct() call is necessary, but I seem to recall that you'd risk duplicates if a given UserProfile were in multiple groups that share the same status. The main point is that filtering on many-to-many relationships works using the usual underscore notation, if you use the names as defined either by related_name or the default related name.

Django get all records of related models

I have 3 models for a to-do list app:
class Topic(models.model)
user = models.ForeignKey(UserProfile)
lists = models.ManyToManyField(List)
class List(models.model)
activities = models.ManyToManyField(Activity)
class Activity(models.model)
activity = models.CharField(max_length=250)
This makes sense when a user selects a Topic, then a List (sub-category), which shows all activities on that list.
But how would I efficiently query things like
All activities of user X (regardless topic or list)
All activities of topic X for user X (regardless of lists)
Would I need to use select_related() in the query and than loop trough the related objects, or is there a more efficient way without looping? (Or should I change my models?)
Use the double-underscore syntax to query across relationships.
All activities for user:
Activity.objects.filter(list__topic__user=my_user)
All activities for user for a topic:
Activity.objects.filter(list__topic=my_topic)
(Note that currently a Topic is for a single user only. Not sure if that's what you mean: you describe a user selecting a topic, which couldn't happen here. Potentially the link from Topic to UserProfile should go the other way, or be a ManyToMany.)
Give them related names (it's easier to manage):
class Topic(models.model)
user = models.ForeignKey(UserProfile, related_name = 'topics')
lists = models.ManyToManyField(List, related_name = 'topics')
class List(models.model)
activities = models.ManyToManyField(Activity, related_name = 'lists')
class Activity(models.model)
activity = models.CharField(max_length=250)
Then you can do awesome things:
user = UserProfile.objects.get(pk=1) # for example
user.topics.all() # returns all topics
topic = Topic.objects.get(pk=1) # for example
topic.users.get(pk=1) # returns user
lists = topic.lists.all() # returns List object instances QuerySet
for list in lists:
list.activites.all()
Handy info: https://docs.djangoproject.com/en/dev/ref/models/relations/

Getting the first item item in a many-to-many relation in Django

I have two models in Django linked together by ManyToMany relation like this:
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person)
I need to get the main person in the group which is the first person in the group. How can I get the first person?
Here's how I'm adding members:
grp = Group.objects.create(name="Group 1")
grp.save()
prs = Person.objects.create(name="Tom")
grp.members.add(prs) #This is the main person of the group.
prs = Person.objects.create(name="Dick")
grp.members.add(prs)
prs = Person.objects.create(name="Harry")
grp.members.add(prs)
I don't think I need any additional columns as the id of the table group_members is a running sequence right.
If I try to fetch the main member of the group through Group.objects.get(id=1).members[0] then Django says that the manager is not indexable.
If I try this Group.objects.get(id=1).members.all().order_by('id')[0], I get the member with the lowest id in the Person table.
How can I solve this?
Thanks
Django doesn't pay attention to the order of the calls to add. The implicit table Django has for the relationship is something like the following if it was a Django model:
class GroupMembers(models.Model):
group = models.ForeignKey(Group)
person = models.ForeignKey(Person)
Obviously, there's nothing there about an "order" or which you should come first. By default, it will probably do as you describe and return the lowest pk, but that's just because it has nothing else to go off of.
If you want to enforce an order, you'll have to use a through table. Basically, instead of letting Django create that implicit model, you create it yourself. Then, you'll add a field like order to dictate the order:
class GroupMembers(models.Model):
class Meta:
ordering = ['order']
group = models.ForeignKey(Group)
person = models.ForeignKey(Person)
order = models.PositiveIntegerField(default=0)
Then, you tell Django to use this model for the relationship, instead:
class GroupMembers(models.Model):
group = models.ForeignKey(Group)
person = models.ForeignKey(Person, through='GroupMembers')
Now, when you add your members, you can't use add anymore, because additional information is need to complete the relationship. Instead, you must use the through model:
prs = Person.objects.create(name="Tom")
GroupMembers.objects.create(person=prs, group=grp, order=1)
prs = Person.objects.create(name="Dick")
GroupMembers.objects.create(person=prs, group=grp, order=2)
prs = Person.objects.create(name="Harry")
GroupMembers.objects.create(person=prs, group=grp, order=3)
Then, just use:
Group.objects.get(id=1).members.all()[0]
Alternatively, you could simply add a BooleanField to specify which is the main user:
class GroupMembers(models.Model):
group = models.ForeignKey(Group)
person = models.ForeignKey(Person)
is_main_user = models.BooleanField(default=False)
Then, add "Tom" like:
prs = Person.objects.create(name="Tom")
GroupMembers.objects.create(person=prs, group=grp, is_main_user=True)
And finally, retrieve "Tom" via:
Group.objects.get(id=1).members.filter(is_main_user=True)[0]
Relational database don't have any notion of ordering, by default. There's no such thing as "first". You need to add an explicit "order" field which keeps the order, and order by it.
You were pretty close:
Group.objects.get(id=1).members.all()[0]
(But as Alex Gaynor says in his answer, for predictable behaviour you need a proper field to sort on)
to reliably keep a record of who is the "main" person in a group, you may want to have a look at using a through table to keep this metadata

Categories