Django Model ManyToMany Reverse Filter

2019-08-08 08:33发布

问题:

Here's excerpts from (something analogous to) my models:

class Person(models.Model):
  name = models.CharField(max_length=20)
  relationships = models.ManyToManyField('self',
    through='Relationship',
    symmetrical=False,
    related_name='related_to',
  )
  def __str__(self):
    return self.name

class Relationship(models.Model):
  from_person = models.ForeignKey(Person,
    related_name='from_people',
    on_delete=models.CASCADE,
  )
  to_person = models.ForeignKey(Person,
    related_name='to_people',
    on_delete=models.CASCADE,
  )
  status = models.CharField(max_length=20)
  def __str__(self):
    return "{} is {} {}".format(
      self.from_person.name, self.status, self.to_person.name)

Here's the contents of my database:

>>> Person.objects.all()
<QuerySet [<Person: A>, <Person: B>, <Person: C>]>
>>> Relationship.objects.all()
<QuerySet [<Relationship: B is Following C>]>

If I want to see who a given person is following, I can build a new method into the Person class:

def get_following(self):
  return self.relationships.filter(
    to_people__status='Following',
    to_people__from_person=self)

This works:

>>> p2.get_following()
<QuerySet [<Person: C>]>

I want to do the REVERSE of this. Instead of asking "Who does this person follow?", I want to ask "Who follows this person?". I can do that like this (although it returns Relationship objects, not Person objects):

>>> Relationship.objects.filter(to_person=p3, status='Following')
<QuerySet [<Relationship: B is Following to C>]>

My attempt is this (which returns an empty QuerySet):

def get_following(self):
  return self.relationships.filter(
    from_people__status='Following',
    from_people__to_person=self)

Your help is appreciated!

EDIT: Here's the ANSWER I chose:

def get_followers(self):
  return self.related_to.filter(from_people__status='Following')

回答1:

Just in case somebody else needs another way of implementing "Followers" exactly like you described, but with a different modelling scheme:

# project/account/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from model_utils import Choices

class User(AbstractBaseUser, PermissionsMixin):
    class Meta:
        # ...

    username = models.CharField(...)
    email = models.EmailField(...)
    # ...

    ### Custom app-specific relationships (database scheme) ###

    # symmetrical=False is needed for this reason: https://stackoverflow.com/a/42040848/3433137
    following = models.ManyToManyField('self', related_name='followers', blank=True, symmetrical=False)
# project/account/forms.py
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.admin.widgets import FilteredSelectMultiple
from .models import User

class UserChangeForm(forms.ModelForm):
    # ...

    # new:
    following = forms.ModelMultipleChoiceField(
        queryset=User.objects.all(),
        required=False,
        widget=FilteredSelectMultiple(
            verbose_name='Following',
            is_stacked=False
        )
    )
    # new:
    followers = forms.ModelMultipleChoiceField(
        queryset=User.objects.all(),
        required=False,
        widget=FilteredSelectMultiple(
            verbose_name='Followers',
            is_stacked=False
        )
    )

    class Meta:
        model = get_user_model()
        # add 'following' and 'followers' to the fields:
        fields = ('email', 'password', ..., 'following', 'followers')

    # also needed to initialize properly:
    def __init__(self, *args, **kwargs):
        super(UserChangeForm, self).__init__(*args, **kwargs)

        # Filter out the self user in the lists and initialize followers list:
        if self.instance and self.instance.pk:
            self.fields['following'] = forms.ModelMultipleChoiceField(
                queryset=User.objects.all().exclude(pk=self.instance.pk),
                required=False,
                widget=FilteredSelectMultiple(
                    verbose_name='Following',
                    is_stacked=False
                )
            )
            self.fields['followers'] = forms.ModelMultipleChoiceField(
                queryset=User.objects.all().exclude(pk=self.instance.pk),
                required=False,
                widget=FilteredSelectMultiple(
                    verbose_name='Followers',
                    is_stacked=False
                )
            )
            self.fields['followers'].initial = self.instance.followers.all()
# project/account/admin.py
from django.contrib.auth import get_user_model
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
from .forms import UserChangeForm, UserCreationForm


class Admin(BaseUserAdmin):
    add_form = UserCreationForm
    form = UserChangeForm
    model = get_user_model()

    # The fields to be used in displaying the User model.
    # These override the definitions on the base UserAdmin
    # that reference specific fields on auth.User.
    list_display = ['email', 'username', 'is_admin']
    list_filter = ('is_admin',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('username',)}),
        ('Permissions', {'fields': ('is_admin', 'is_superuser', 'is_staff')}),
        # new:
        ('Following / Followers', {'fields': ('following', 'followers')}),
    )
    # other fields
    # ...

    # new:
    filter_horizontal = ('following', 'followers')

admin.site.register(User, Admin)
admin.site.unregister(Group)

If you then start the server and go to localhost:8000/admin/ and navigate to a user's detail-page, you should see something like this on your screen:

I did not add counters so you are able to see the amount of followers at once in the list_view.

Note that the second FormField with the followers is read-only in the admin panel. A user cannot choose other users to follow him.



回答2:

You got a QuerySet like this: <QuerySet [<Relationship: B is Following C>]>. Think that one day (I guess that it is the propose of that) 'person' has a lot of followers and it probably might return so many followers, like this: <QuerySet [<Relationship: B is Following C>, <Relationship: A is Following C>]>. So, I would use values_list() [1]:

Relationship.objects.filter(to_person=p3, status='Following').values_list('from_person__name', flat=True)

Returns: <QuerySet [A, B, ...]>

If you only pass in a single field, you can also pass in the flat parameter. If True, this will mean the returned results are single values, rather than one-tuples.

or create a method:

def get_followers(self):
    follower_of_person = []
    for value in relation_of_person:
        follower_of_p3.append(value.from_person.name)
    return follower_of_person

Returns: [A, B, ...]

values_list still is better, because you are working directly on the database.

[1] https://docs.djangoproject.com/en/2.0/ref/models/querysets/#values-list (Here have a good example of ManyToMany).



回答3:

Take a look at this document. Otherwise here are some other methods...

self.relationships.from_people.objects.all() would return all objects with of with the related name from_people.

I would, however, slightly change some code such that the use would be self.relationships.from_people.objects.filter(status='Following')

Another way to do it (though not the most efficient) is to pass in the person model, and pass in the relationships model using the person.pk as a filter.

def get_following(self, pk):
    person = Person.objects.get(pk=pk)
    relationships = Relationship.objects.filter(to_person=person.id)
    return relationships


回答4:

Here's the ANSWER I chose:

def get_followers(self):
  return self.related_to.filter(from_people__status='Following')