【问题标题】:How to create a model field that is an average of all of another foreign key model' field. Ex: avg rating field如何创建一个模型字段,它是所有另一个外键模型字段的平均值。例如:平均评分字段
【发布时间】:2021-07-01 12:29:02
【问题描述】:

我有两个模型。小说模型可以是任何电影、书籍、电视剧或类似的东西。我有另一个模型,它是包含类似评论的字段的评论,例如:评论者、评级、描述。 我想要做的是以下内容: 在虚构模型中有两个额外的字段:

  1. 评论数
  2. 平均评分

我能够将它们添加为整数和浮点字段,然后在添加、编辑或删除新评论时更改它们,但有两个问题。

  1. 不会考虑添加来自管理员的评论
  2. 我只是觉得这不是最好的方法,而且我觉得有一种更明智的方法可以从审查模型中自动填充字段。 这是两个模型以及我如何实现 api 视图。

小说模型.py

    from django.db import models

    from stream.models import StreamPlatform


   class Fiction(models.Model):
    """
    Model that encopasses a Movie, TV Series, book or similar
    """
    MOVIE       = 1
    TV_SERIES   = 2
    BOOK        = 3
    PODCAST     = 4

    TYPE = (
        (MOVIE, 'Movie'),
        (TV_SERIES, 'TV-Series'),
        (BOOK, 'Book'),
        (PODCAST, 'Podcast')
    )


    title = models.CharField(max_length=50)
    description = models.CharField(max_length=200)
    active = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)
    avg_rating = models.FloatField(default=0)
    number_rating = models.IntegerField(default=0)
    platform = models.ForeignKey(
        StreamPlatform, 
        on_delete=models.SET_NULL, 
        related_name='fictions',
        null = True
        )
    type = models.PositiveSmallIntegerField(
        choices = TYPE,
        default = MOVIE
    )

    def __str__(self):
        return self.title

查看 models.py

    from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator

from django.contrib.auth.models import User

from fiction.models import Fiction

class Review(models.Model):
    """
    model for fiction reviews from users
    """
    reviewer = models.ForeignKey(User, on_delete=models.CASCADE)
    rating = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
    fiction = models.ForeignKey(Fiction, on_delete=models.CASCADE, related_name="reviews")
    description = models.CharField(max_length=200, null = True, blank =True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return str(self.rating) + " | " + str(self.fiction)

    class Meta:
        unique_together = ['reviewer', 'fiction']
        ordering = ['-created']

查看views.py

    from watchmate.permissions import IsAdminOrReadOnly, IsOwnerOrReadOnly
from rest_framework import generics, mixins
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticatedOrReadOnly

from django.db.utils import IntegrityError

from fiction.models import Fiction

from .models import Review
from .serializers import ReviewSerializer

class ReviewList(generics.ListCreateAPIView):
    permission_classes = [IsAdminOrReadOnly]
    # queryset = Review.objects.all()
    serializer_class = ReviewSerializer

    def get_queryset(self):
        pk = self.kwargs['pk']
        return Review.objects.filter(fiction=pk)

    def perform_create(self, serializer):
        pk = self.kwargs.get('pk')
        fiction = Fiction.objects.get(pk=pk)

        reviewer = self.request.user

        # check if user has already reviewd this fiction
        # review = Review.objects.filter(fiction=fiction, reviewer=reviewer)
        # if review.exists():
        #     raise ValidationError("You have already reviewed this fiction")
        
        try:
            serializer.save(fiction=fiction, reviewer=reviewer)

            if fiction.number_rating == 0:
                fiction.avg_rating = serializer.validated_data['rating']
            else:
                fiction.avg_rating = (serializer.validated_data['rating']+fiction.avg_rating)/2

            fiction.number_rating += 1
            fiction.save()


        except IntegrityError:
            raise ValidationError("You have already reviewed this fiction") 


class ReviewDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Review.objects.all()
    serializer_class = ReviewSerializer
    permission_classes = [IsOwnerOrReadOnly]


    def perform_update(self, serializer):
        instance = self.get_object()
        pk = self.kwargs.get('fiction_pk')
        fiction = Fiction.objects.get(pk=pk)
        # calculate overall rating sums
        total_rating = fiction.avg_rating*fiction.number_rating
        # subtract old rating
        total_rating -= instance.rating
        # add new rating
        total_rating += serializer.validated_data['rating']
        # calculate new fiction avg rating and save it
        fiction.avg_rating = total_rating/fiction.number_rating
        fiction.save()
        super().perform_update(serializer)

    def perform_destroy(self, instance):
        instance = self.get_object()
        pk = self.kwargs.get('fiction_pk')
        fiction = Fiction.objects.get(pk=pk)
        # calculate overall rating sums
        total_rating = fiction.avg_rating*fiction.number_rating
        # subtract old rating
        total_rating -= instance.rating
        # decrease fiction reviews by one
        fiction.number_rating -= 1
        # calculate new fiction avg rating and save it
        fiction.avg_rating = total_rating/fiction.number_rating
        fiction.save()
        super().perform_destroy(instance)

添加新评论的逻辑不是 100% 正确,但没关系。我只是想知道如何能够在模型和字段级别实现此逻辑,因此也可以使用管理员添加评论并且仍然能够看到更改

【问题讨论】:

  • 不要使用字段。您可以使用属性或注释,但不要具体化数据库中的聚合数据。
  • 是否可以告诉我如何添加这样的属性或注释?我真的很感激

标签: python django django-models django-rest-framework django-views


【解决方案1】:

正如威廉的评论中所说,您可以使用@property。以下是官方文档中的示例:

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()

def baby_boomer_status(self):
    "Returns the person's baby-boomer status."
    import datetime
    if self.birth_date < datetime.date(1945, 8, 1):
        return "Pre-boomer"
    elif self.birth_date < datetime.date(1965, 1, 1):
        return "Baby boomer"
    else:
        return "Post-boomer"

    @property
    def full_name(self):
        "Returns the person's full name."
        return '%s %s' % (self.first_name, self.last_name)

【讨论】:

    【解决方案2】:

    如果有人想要解决这样的问题,我就是像@Willem Van Onsem 和@Mhamed Bendenia 所说的那样,通过向方法添加属性来解决我的问题。

    @property
    def number_of_ratings(self):
        return self.reviews.count()
    
    @property 
    def average_rating(self):
        return self.reviews.aggregate(Avg('rating'))['rating__avg']
    

    这样我就不必在每个视图中自己编写逻辑

    【讨论】:

      猜你喜欢
      • 2021-04-30
      • 1970-01-01
      • 2012-05-20
      • 1970-01-01
      • 2016-01-02
      • 2020-06-19
      • 2018-09-21
      • 2017-04-25
      • 2015-10-06
      相关资源
      最近更新 更多