How do I model a symmetric relationship with django? - django

Let's use the classic example of friends.
class Friendship(models.Model):
user1 = models.ForeignKey(User, related_name='friends1')
user2 = models.ForeignKey(User, related_name='friends2')
handshakes = models.PositiveIntegerField()
hugs = models.PositiveIntegerField()
# other silly data
Two friends in a friendship (user1 and user2) should be completely equal. I should be able to say that (user1, user2) are unique_together and not have to worry about (user2, user1) accidentally showing up. I should be able to get all the friends of a given user easily, but instead I'd have to write a custom manager or create some other way of getting all the Friendships where that user is user1 in the relationship, and all the Friendships where that user is user2.
I'm considering trying to write my own SymmetricKey. Someone please stop me.

Check out the symmetrical option of ManyToManyField in the docs -- sounds like it can do what you want.
For the specific way you're doing it, I'd do something like
class LameUserExtension(User):
friends = ManyToManyField("self", through=Friendship)
class Friendship(models.Model):
# the stuff you had here

I found a nice article discussing that some time ago, the basics are the following:
class Person(models.Model):
name = models.CharField(max_length=100)
relationships = models.ManyToManyField('self', through='Relationship',
symmetrical=False,
related_name='related_to+')
RELATIONSHIP_FOLLOWING = 1
RELATIONSHIP_BLOCKED = 2
RELATIONSHIP_STATUSES = (
(RELATIONSHIP_FOLLOWING, 'Following'),
(RELATIONSHIP_BLOCKED, 'Blocked'),
)
class Relationship(models.Model):
from_person = models.ForeignKey(Person, related_name='from_people')
to_person = models.ForeignKey(Person, related_name='to_people')
status = models.IntegerField(choices=RELATIONSHIP_STATUSES)
Note the plus-sign at the end of related_name. This indicates to Django that the reverse relationship should not be exposed. Since the relationships are symmetrical, this is the desired behavior, after all, if I am friends with person A, then person A is friends with me. Django won't create the symmetrical relationships for you, so a bit needs to get added to the add_relationship and remove_relationship methods to explicitly handle the other side of the relationship:
def add_relationship(self, person, status, symm=True):
relationship, created = Relationship.objects.get_or_create(
from_person=self,
to_person=person,
status=status)
if symm:
# avoid recursion by passing `symm=False`
person.add_relationship(self, status, False)
return relationship
def remove_relationship(self, person, status, symm=True):
Relationship.objects.filter(
from_person=self,
to_person=person,
status=status).delete()
if symm:
# avoid recursion by passing `symm=False`
person.remove_relationship(self, status, False)
Now, whenever we create a relationship going one way, its complement is created (or removed). Since the relationships go in both directions, we can simply use:
def get_relationships(self, status):
return self.relationships.filter(
to_people__status=status,
to_people__from_person=self)
Source: Self-referencing many-to-many through

Related

Django complex query filter through reverse relationships

I have two models:
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(db_index=True, max_length=20, unique = True)
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
full_name = models.CharField(db_index=True, max_length=256, null=True)
friends = models.ManyToManyField("self", related_name = 'friends', symmetrical=True)
For search purposes, I need a query to:
SELECT all friends and friends of friends (excluding self)
WHERE username__startswith(query) OR full_name__startswith(query)
I can't figure out how to do that given the structure: need to access friends, then friends of friends and reverse User objects. The question therefore is:
Can I avoid looping through the results and apply a filter to make the database do the work?
Why is UserProfile a separate class? Seems like those fields belong to a User.
You don't ask a question, so I'm almost certainly going to provide a partial answer, at best. Hopefully it will get you started.
To access friends you could:
profile = UserProfile.objects.get(pk=1)
friends = UserProfile.friends.all().prefetch_related('friends')
for friend in friends:
f_user = friend.user
f_friends = friend.friends.all().prefetch_related('friends')
This is going to be inefficient at scale, but fine for single UserProfiles. Otherwise a cached_property might be the way to go. In fact, the docs seem similar to your issue.
If you're getting friends from a queryset of UserProfiles, you should .prefetch_related('friends')
class UserProfile(models.Model):
...
#cached_property
def my_friends(self):
return [f for f in self.friends.all().prefetch_related('friends')]
Using a list comprehension like this saves you having a DB hit every time, if I'm not mistaken.
Since friends are symmetrical, perhaps this:
profile = UserProfile.objects.get(user=<some_user>)
all_profiles = UserProfile.objects.filter(Q(user__username__startswith=query) |
Q(full_name__startswith=query)).distinct()
profile_friends = all_profiles.filter(friends=profile)
friends_of_friends = all_profiles.filter(friends__in=profile_friends)
all_friends = profile_friends | friends_of_friends
I haven't tested this, but I think it's pretty close. There are likely to be some duplications, so a final distinct may need to be applied.

Which pattern to use for a model to have different variations of? Generic FK, multi-table, inheritance, others?

I am having trouble deciding how to structure my models for a particular data structure.
The models I have would be Posts, Groups, Users.
I want the Post model that can be posted from a groups page or user page and potentially more, like an events page.
Posts would contain fields for text, images(fk), user, view count, rating score (from -- a reference to where ever it was posted from like user or group page, though I am unsure how to make this connection yet)
I thought about using a Generic Foreign Key to assign a field to different models but read articles suggesting to avoid it. I tried the suggested models, but I wasn't unsure if they were the right approach for what I required.
At the moment I went with Alternative 4 - multi-table inheritance
class Group(models.Model):
name = models.CharField(max_length=64)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='_groups')
members = models.ManyToManyField(
settings.AUTH_USER_MODEL)
def __str__(self):
return f'{self.name} -- {self.created_by}'
def save(self, *args, **kwargs):
# https://stackoverflow.com/a/35647389/1294405
created = self._state.adding
super(Group, self).save(*args, **kwargs)
if created:
if not self.members.filter(pk=self.created_by.pk).exists():
self.members.add(self.created_by)
class Post(models.Model):
content = models.TextField(blank=True, default='')
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s_posts",
related_query_name="%(app_label)s_%(class)ss")
# class Meta:
# abstract = True
def __str__(self):
return f'{self.content} -- {self.created_by}'
class PostImage(models.Model):
image = models.ImageField(upload_to=unique_upload)
post = models.ForeignKey(
Post, related_name='images', on_delete=models.CASCADE)
def __str__(self):
return '{}'.format(self.image.name)
class UserPost(models.Model):
post = models.OneToOneField(
Post, null=True, blank=True, related_name='_uPost', on_delete=models.CASCADE)
class GroupPost(models.Model):
post = models.OneToOneField(
Post, null=True, blank=True, related_name='_gPost', on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
To do some specific filters ex:
Filter specific group post
Post.objects.filter(_gPost__group=group)
Filter specific user post
Post.objects.filter(created_by=user) # exclude groups with _gPost__isnull=False
Create post to user/group
p = Post.objects.create(...)
up = UserPost.objects.create(post=p)
gp = GroupPost.objects.create(post=p)
Really I am wondering if this is a sensible approach. The current way of a filter and creating feel odd. So only thing making me hesitant on this approach is just how it looks.
So, is Generic ForeignKey the place to use here or the current multi-table approach. I tried going with inheritance with abstract = True and that was unable to work as I need a foreign key to base post model. Even with no abstract, I got the foreign key reference, but filter became frustrating.
Edit:
So far only weird issues(but not really) are when filtering I have to be explicit to exclude some field to get what I want, using only .filter(created_by=...) only would get all other intermediate tables.
Filter post excluding all other tablets would requirePost.objects.filter(_uPost__isnull=True, _gPost__isnull=True, _**__isnull=True) which could end up being tedious.
I think your approach is sensible and that's probably how I would structure it.
Another approach would be to move the Group and Event foreignkeys into the Post model and let them be NULL/None if the Post wasn't posted to a group or event. That improves performance a bit and makes the filters a bit more sensible, but I would avoid that approach if you think Posts can be added to many other models in the future (as you'd have to keep adding more and more foreignkeys).
At the moment I will stick with my current pattern.
Some extra reading for anyone interested.
https://www.slideshare.net/billkarwin/sql-antipatterns-strike-back/32-Polymorphic_Associations_Of_course_some

Django many-to-many lookup from different models

I have some models that represents some companies and their structure. Also all models can generate some Notifications (Notes). User can see own Notes, and, of course, can't see others.
class Note(models.Model):
text = models.CharField(...)
class Company(models.Model):
user = models.ForeignKey(User)
note = models.ManyToManyField(Note, blank='True', null='True')
class Department(models.Model):
company = models.ForeignKey(Company)
note = models.ManyToManyField(Note, blank='True', null='True')
class Worker(models.Model):
department = models.ForeignKey(Department)
note = models.ManyToManyField(Note, blank='True', null='True')
class Document(models.Model)
company = models.ForeignKey(Company)
note = models.ManyToManyField(Note, blank='True', null='True')
The question is how I can collect all Notes for particular user to show them?
I can do:
Note.objects.filter(worker__company__user=2)
But its only for Notes that was generated by Workers. What about another? I can try hardcoded all existing models, but if do so dozen of kittens will die!
I also tried to use backward lookups but got "do not support nested lookups". May be I did something wrong.
EDIT:
As I mentioned above I know how to do this by enumerating all models (Company, Worker, etc. ). But if I will create a new model (in another App for example) that also can generate Notes, I have to change code in the View in another App, and that's not good.
You can get the Notes of a user by using the following query:
For example let us think that a user's id is 1 and we want to keep it in variable x so that we can use it in query. So the code will be like this:
>>x = 1
>>Note.objects.filter(Q(**{'%s_id' % 'worker__department__company__user' : x})|Q(**{'%s_id' % 'document__company__user' : x})|Q(**{'%s_id' % 'company__user' : x})|Q(**{'%s_id' % 'department__company__user' : x})).distinct()
Here I am running OR operation using Q and distinct() at the end of the query to remove duplicates.
EDIT:
As I mentioned above I know how to do this by enumerating all models
(Company, Worker, etc. ). But if I will create a new model (in another
App for example) that also can generate Notes, I have to change code
in the View in another App, and that's not good.
In my opinion, if you write another model, how are you suppose to get the notes from that model without adding new query? Here each class (ie. Department, Worker) are separately connected to Company and each of the classes has its own m2m relation with Note and there is no straight connection to User with Note's of other classes(except Company). Another way could be using through but for that you have change the existing model definitions.
Another Solution:
As you have mentioned in comments, you are willing to change the model structure if it makes your query easier, then you can try the following solution:
class BaseModel(models.Model):
user = models.Foreignkey(User)
note = models.ManyToManyField(Note)
reports_to = models.ForeignKey('self', null=True, default=None)
class Company(BaseModel):
class Meta:
proxy = True
class Document(BaseModel):
class Meta:
proxy = True
#And so on.....
Advantages: No need to create separate table for document/company etc.
object creation:
>>c= Company.objects.create(user_id=1)
>>c.note.add(Note.objects.create(text='Hello'))
>>d = Document.objects.create(user_id=1, related_to=c)
>>d.note.add(Note.objects.create(text='Hello World'))

Django Models: Subclassing approach?

ists,
I'm looking for some validation on a subclassing approach. I have the following:
class Person(models.Model):
"""
Basic person
"""
user = models.ForeignKey(User) # hide
first_name = models.CharField(max_length=200)
last_name = models.CharField(blank=True, max_length=200)
class Meta:
verbose_name_plural = "People"
def __unicode__(self):
return u"%s, (%s)" % (self.first_name, self.user)
class Contributor(Person):
"""
Contributor
A Core contributor of the site content workflow
"""
class Meta:
verbose_name = 'contributor'
verbose_name_plural = 'contributors'
def get_articles(self):
"""
Return the articles that the author has published.
"""
return Article.objects.filter(self_in=authors)
class Member(Person):
"""
Member
A Member of the website.
"""
# Member history, payments etc...
joined = models.DateTimeField()
So, each Member or Contributor is a Person within the system, but it is possible for a Person to be 'None', 1 or both Member & Contributor, depending on their context.
This subclassing approach makes it simple to do things like:
#...
contributors = models.ManyToManyField(Contributor, help_text="Contributors/Authors to this article")
or
print Member.objects.all()
... and of course the usual efficiencies of subclassing, i.e. common fields and methods.
However, I'm wondering about the pros & cons of doing something like
class Person(models.Model):
"""
Person
"""
user = models.ForeignKey(User) # hide
first_name = models.CharField(max_length=200)
last_name = models.CharField(blank=True, max_length=200)
is_contributor = models.BooleanField()
is_member = models.BooleanField()
but then needing to filter things like
# Assuming this is possible...
contributors = models.ManyToManyField(Person.objects.filter(is_contributor=True), help_text="Contributors/Authors to this article")
With the subclassing approach, I wonder about the challenges of being aware of users that are People (Person), Members or Contributors - and being able to discern between.
i.e. its really easy to do if person.is_contributor: but perhaps more challenging
try:
Contributor.objects.get(person__user_id=request.user.id)
except:
no_access()
else:
let_them_in()
Apologies for the open-endness of this question -- it may have been more an opportunity to think out aloud.
First, there are two oddities about your model to begin with:
1) Why is Person -=> User a ForeignKey and not a OneToOne? Might a user be more than one person?
2) User already has first and last names - why also assign them to person?
Next, to the extent that your ultimate goal is the authorization depicted at the end, why not just use permissions? Then you won't need the boolean fields or the try - except at the end.
Fundamentally, I see nothing wrong with subclassing the User model. Folks in #django often fight over this, but if done right, it is one of the most time-saving and powerful steps you can take when you first sit down with your new django project.
Adding different subclasses of User with different attributes and different methods can very quickly give you a robust user environment with enormous auth possibilities. Thus far, however, it doesn't look like you have done anything that requires you to subclass User.

Checking another field at the same time as checking a many-to-many relationship

I have a simple-ish ownership design on one of my models. It can be owned by multiple people and current owners can add other people but they have to confirm the invite before they are treated as a real owner.
class MyOwnedThing(models.Model):
owners = models.ManyToManyField(User, through='Ownership', related_name='othings')
def is_owner(self, user):
return user in self.owners
class Ownership(models.Model):
user = models.ForeignKey(User)
myownedthing = models.ForeignKey(MyOwnedThing)
confirmed = models.BooleanField(default=False)
The problem is MyOwnedThing.is_owner needs to check that the owner had confirmed their invite. Is there a simple way of doing that or am I left doing a separate try/except around Ownership.objects.filter(user=u, myownedthing=mot, confirmed=True)?
I typically use association tables for this type of functionality. The following hasn't been tested but should give you a general idea of what I mean:
class Resource(models.Model):
resource = models.TextField(max_length=255)
class ResourceUser(models.Model):
owner = models.ForeignKey(User)
resource = models.ForeignKey(Resource)
def is_owner(self, user, res):
return self.filter(self.owner=user).filter(self.resource=res)