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), но пока что это работает