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

Вернуться на верх