Как фильтровать несколько полей со списком объектов

Я хочу создать webapp, например Quora или Medium, где пользователь может следить за пользователями или некоторыми темами.

например: userA следует (userB, userC, tag-Health, tag-Finance).

Это модели:

class Relationship(models.Model):
    user = AutoOneToOneField('auth.user')
    follows_user = models.ManyToManyField('Relationship', related_name='followed_by')
    follows_tag = models.ManyToManyField(Tag)

class Activity(models.Model):
    actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
    actor_id = models.PositiveIntegerField()
    actor = GenericForeignKey('actor_type', 'actor_id')
    verb = models.CharField(max_length=10)
    target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
    target_id = models.PositiveIntegerField()
    target = GenericForeignKey('target_type', 'target_id')
    tags = models.ManyToManyField(Tag)

Теперь это даст следующий список:

following_user = userA.relationship.follows_user.all()
following_user
[<Relationship: userB>, <Relationship: userC>]
following_tag = userA.relationship.follows_tag.all()
following_tag
[<Tag: tag-job>, <Tag: tag-finance>]

Чтобы фильтровать, я пробовал этот путь:

Activity.objects.filter(Q(actor__in=following_user) | Q(tags__in=following_tag))

Но так как актер является GenericForeignKey, я получаю сообщение об ошибке:

FieldError: Field 'actor' не генерирует автоматическое обратное отношение и поэтому не может использоваться для обратного запроса. Если это GenericForeignKey, подумайте о добавлении GenericRelation.

Как я могу отфильтровать действия, которые будут уникальными, со списком пользователей и списком тегов, которые следуют пользователю? Чтобы быть конкретным, как я буду фильтровать GenericForeignKey со списком объектов, чтобы получить действия следующих пользователей.

Ответ 1

То, что предлагают docs, не так уж плохо.

Проблема заключается в том, что при создании Activities вы используете auth.User как actor, поэтому вы не можете добавить GenericRelation в auth.User (ну, может быть, вы можете его обезвредить, но это не очень хорошая идея).

Итак, что вы можете сделать?

Решение

@Sardorbek Imomaliev очень хорошее, и вы можете сделать это еще лучше, если поместить всю эту логику в пользовательский класс QuerySet. (идея состоит в том, чтобы достичь СУХОЙ И ВОЗМОЖНОСТИ)

class ActivityQuerySet(models.QuerySet):
    def for_user(self, user):
        return self.filter(
            models.Q(
                actor_type=ContentType.objects.get_for_model(user),
                actor_id__in=user.relationship.follows_user.values_list('id', flat=True)
            )|models.Q(
                tags__in=user.relationship.follows_tag.all()
            )
        )

class Activity(models.Model):
    #..
    objects = ActivityQuerySet.as_manager()

#usage
user_feed = Activity.objects.for_user(request.user)

, но есть ли что-нибудь еще?

1. Вам действительно нужно GenericForeignKey для actor? Я не знаю вашей бизнес-логики, так что, возможно, вы это делаете, но используя только обычный FK для актера (как и для tags), вы сможете сделать такой персонал, как actor__in=users_following.

2. Проверено, нет ли для этого приложения? Одним из примеров пакета, уже решая вашу проблему, является django-activity-steam, чтобы проверить его.

3. Если вы не используете auth.User как actor, вы можете сделать именно то, что предлагает docs → добавить поле GenericRelation. На самом деле, ваш класс Relationship подходит для этой цели, но я бы действительно переименовал его в нечто вроде UserProfile или по крайней мере UserRelation. Рассмотрим, что мы переименовали Relation в UserProfile, и вместо этого мы создаем новый Activities, используя UserProfile. Идея такова:

class UserProfile(models.Model):
    user = AutoOneToOneField('auth.user')
    follows_user = models.ManyToManyField('UserProfile', related_name='followed_by')
    follows_tag = models.ManyToManyField(Tag)

    activies_as_actor = GenericRelation('Activity',
        content_type_field='actor_type',
        object_id_field='actor_id',
        related_query_name='userprofile'
    )


class ActivityQuerySet(models.QuerySet):
    def for_userprofile(self, userprofile):
        return self.filter(
            models.Q(
                userprofile__in=userprofile.follows_user.all()
            )|models.Q(
                tags__in=userprofile.relationship.follows_tag.all()
            )
        )


class Activity(models.Model):
    #..
    objects = ActivityQuerySet.as_manager()

#usage

#1st when you create activity use UserProfile
Activity.objects.create(actor=request.user.userprofile, ...)

#2nd when you fetch. 
#Check how `for_userprofile` is implemented this time
Activity.objects.for_userprofile(request.user.userprofile)

Ответ 2

Вы должны просто фильтровать идентификаторы.

Сначала получите идентификаторы объектов, которые вы хотите фильтровать на

following_user = userA.relationship.follows_user.all().values_list('id', flat=True)
following_tag = userA.relationship.follows_tag.all()

Также вам нужно будет фильтровать actor_type. Это можно сделать следующим образом например.

actor_type = ContentType.objects.get_for_model(userA.__class__)

Или как @Todor предложил в комментариях. Поскольку get_for_model принимает как экземпляр модели, так и экземпляр модели

actor_type = ContentType.objects.get_for_model(userA)

И чем вы можете просто фильтровать вот так.

Activity.objects.filter(Q(actor_id__in=following_user, actor_type=actor_type) | Q(tags__in=following_tag))

Ответ 3

Как указано в документации:

Из-за того, как реализован GenericForeignKey, вы не можете использовать такие поля непосредственно с фильтрами (например, filter() и exclude()) через API базы данных. Поскольку GenericForeignKey не является обычным полевым объектом, эти примеры не будут работать:

Вы можете следить за сообщением об ошибке, я думаю, вам нужно будет добавить отношение GenericRelation для этого. У меня нет опыта в этом, и я должен изучить его, но...

Лично я считаю, что это решение слишком сложно для того, чего вы пытаетесь достичь. Если только модель user может следовать за тегом или авторами, почему бы не включить в нее многопользовательский интерфейс. Это будет примерно так:

class Person(models.Model):
    user = models.ForeignKey(User)
    follow_tag = models.ManyToManyField('Tag')
    follow_author = models.ManyToManyField('Author')

Вы можете запросить все последующие действия тегов для каждого человека следующим образом:

Activity.objects.filter(tags__in=person.follow_tag.all())

И вы можете искать "лиц", следующих за тегом:

Person.objects.filter(follow_tag__in=[<tag_ids>])

То же самое относится к авторам, и вы можете использовать querysets для выполнения OR, AND и т.д. в ваших запросах.

Если вы хотите, чтобы другие модели могли отслеживать тег или автора, скажем, System, возможно, вы могли бы создать модель Following, которая делает то же самое, что и Person, а затем вы можете добавить ForeignKey до Follow как в Person, так и System

Обратите внимание, что я использую этот Person для выполнения этой рекомендации.

Ответ 4

Вы можете запросить отдельно для обоих usrs и тегов, а затем объединить их, чтобы получить то, что вы ищете. Пожалуйста, сделайте что-нибудь вроде ниже и дайте мне знать, если это сработает.

usrs = Activity.objects.filter(actor__in=following_user)    
tags = Activity.objects.filter(tags__in=following_tag)

result = usrs | tags

Ответ 5

Вы можете использовать аннотацию для объединения двух первичных ключей в виде одной строки, а затем использовать это для фильтрации вашего запроса.

from django.db.models import Value, TextField
from django.db.models.functions import Concat

following_actor = [
    # actor_type, actor
    (1, 100),
    (2, 102),
]
searchable_keys = [str(at) + "__" + str(actor) for at, actor in following_actor]

result = MultiKey.objects.annotate(key=Concat('actor_type', Value('__'), 'actor_id', 
                                              output_field=TextField()))\
    .filter(Q(key__in=searchable_keys) | Q(tags__in=following_tag))