Django DRF: как группировать по внешним полям?
У меня есть модель, в которой пользователи могут голосовать за других пользователей в определенных темах. Что-то вроде:
#models.py
Class Topic(models.Model):
name = models.StringField()
def __str__(self):
return str(self.name)
Class UserUpvotes(models.Model):
"""Holds total upvotes by user and topic"""
user = models.ForeignKey(User)
topic= models.ForeignKey(Topic)
upvotes = models.PositiveIntegerField(default=0)
Используя DRF, у меня есть API, который возвращает следующее: topic_id
, topic_name
и upvotes
, что является общим количеством upvotes для данной темы.
Одно из требований проекта заключается в том, чтобы API использовал именно эти имена полей:
.topic_id
,topic_name
, иupvotes
#serializers.py
class TopicUpvotesSerializer(serializers.ModelSerializer):
topic_name = serializers.StringRelatedField(source="topic")
class Meta:
model = UserUpvotes
fields = ["topic_id", "topic_name", "upvotes"]
Моя проблема заключается в агрегировании этих полей. Я фильтрую UserUpvotes
по пользователю или команде, а затем агрегирую по теме.
Желаемый выход
Вот результат, который я хочу получить. Когда я не выполняю никаких агрегаций (а есть представления, где это будет именно так), это работает.
[
{
"topic_name": 3,
"topic_name": "Korean Studies",
"upvotes": 14
},
{
"topic_name": 12,
"topic_name": "Inflation",
"upvotes": 3
},
]
Сначала я попробовал создать TopicSerializer
, а затем присвоить его полю topic
в TopicUpvotesSerializer
. Но тогда результирующий json имел бы вложенное поле "topic", и агрегирование было бы неудачным.
Попытка 1
#views.py
def get_queryset(self):
return (
UserUpvotes.objects.filter(user__team=team)
.values("topic")
.annotate(upvotes=models.Sum("upvotes"))
.order_by("-upvotes")
)
Моя проблема в том, что поля topic_id
и topic_name
не отображаются. Я получаю что-то вроде:
[
{
"topic_name": "3",
"upvotes": 14
},
{
"topic_name": "12",
"upvotes": 3
},
]
Попытка 2
Еще одна попытка кверисета:
# views.py
def get_queryset(self):
return (
UserUpvotes.objects.filter(user__team=team)
.values("topic__id", "topic__name")
.annotate(upvotes=models.Sum("upvotes"))
.order_by("-upvotes")
)
Что дает:
[
{
"upvotes": 14
},
{
"upvotes": 3
},
]
Агрегация работала на уровне кверисета, но сериализатор не смог найти нужные поля.
Попытка 3
Это самое близкое, что я получил:
# views.py
def get_queryset(self):
return (
UserUpvotes.objects.filter(user__team=team)
.values("topic__id", "topic__name")
.annotate(upvotes=models.Sum("upvotes"))
.values("topic_id", "topic", "upvotes")
.order_by("-upvotes")[:n]
)
[
{
"topic_name": 3,
"topic_name": "3",
"upvotes": 14
},
{
"topic_name": 12,
"topic_name": "12",
"upvotes": 3
},
]
Я понятия не имею, почему "topic_name" просто преобразует "topic_id" в строку, а не вызывает метод string.
Желаемый вывод Это результат, который я хочу получить. Когда я не выполняю никаких агрегаций (а есть представления, где это будет именно так), это работает.
[
{
"topic_name": 3,
"topic_name": "Korean Studies",
"upvotes": 14
},
{
"topic_name": 12,
"topic_name": "Inflation",
"upvotes": 3
},
]
Сериализованный FK всегда даст вам ID
связанной модели. Я не уверен, почему вы называете его topic_name
, если это равнозначно ID
. Теперь, если вы действительно хотите получить поле name
модели Topic
в topic_name = serializers.StringRelatedField(source="topic")
, вы должны дать ему source="topic.name"
Однако, если вы пытаетесь получить ID отношения, вы можете использовать ModelSerializer
:
class TopicUpvotesSerializer(serializers.ModelSerializer):
class Meta:
model = UserUpvotes
fields = "__all__"
Работа с сериализатором для темы:
class TopicSerializer(serializers.ModelSerializer):
upvotes = serializers.IntegerField(read_only=True)
class Meta:
model = Topic
fields = ['id', 'name', 'upvotes']
затем в ModelViewSet
вы аннотируете:
from django.db.models import Sum
from rest_framework.viewsets import ModelViewSet
class TopicViewSet(ModelViewSet):
serializer_class = TopicSerializer
queryset = Topic.objects.annotate(upvotes=Sum('userupvotes__upvotes'))
@willem-van-onsem Ответ Виллема является правильным для данной задачи в том виде, как я его сформулировал.
Но... У меня был другой случай использования (извините! ◑﹏◐), когда API Users использовал сериализатор UserUpvotes
в качестве вложенного поля. Поэтому мне пришлось искать другое решение. В итоге я пришел к такому решению. Выкладываю на случай, если это кому-то поможет.
class UserUpvotesSerializer(serializers.ModelSerializer): topic_name = serializers.SerializerMethodField()
def get_topic_name (self, obj):
try:
_topic_name = obj.topic.name
except TypeError:
_topic_name = obj.get("skill__name", None)
return _topic_name
class Meta:
model = UserUpvotes
fields = ["topic_id", "topic_name", "upvotes"]
Я все еще не знаю почему поле SerializerMethodField
работает, а поле StringRelatedField
нет. Это похоже на ошибку?
В любом случае, проблема заключается в том, что после агрегации values().annotate()
, obj
уже не QuerySet, а dict. Поэтому прямой доступ к объекту name
приведет к ошибке 'UserUpvotes' object is not subscriptable.
Я не знаю, есть ли еще какие-нибудь крайние случаи, о которых я должен знать (это тот случай, когда мне РЕАЛЬНО не хватает подсказок типов в Django), но пока что это работает