Как фильтровать несколько полей со списком объектов
Я хочу создать 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))