Система отзывов с оценкой
Всем привет!) Относительно недавно начал изучать фреймворк Django и в качестве проекта захотел написать полноценный сайт отзывов. Столкнулся с одной проблемой, что сделал модели самого Поста (в котором описывают, например, фирму и т.д.) + Отзывы к этому посту + Категории услуг, но не получается вывести среднюю оценку каждого поста исходя из оценок отзывов. Уже 3 дня сижу и не могу понять как сделать адекватную функцию по выводу средней оценки каждого поста. Models.py у меня выглядит так:
DEFAULT_CHOICES = (
('5', 'Отлично'),
('4', 'Хорошо'),
('3', 'Нормально'),
('2', 'Плохо'),
('1', 'Ужасно'),
)
class Post(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
('published', 'Published'),
)
category = models.ForeignKey(Category, related_name='posts', on_delete=models.CASCADE)
title = models.CharField(max_length=250) # заголовок статьи
slug = models.SlugField(max_length=250, unique_for_date='publish') # формирования URL-ов в виде букв и цифр для улучшения распознавания поисковой системой
image = models.ImageField(upload_to='blog/posts_images/', blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts') # автор публикации
body = models.TextField() # сама статья
publish = models.DateTimeField(default=timezone.now) # время когда опубликовано
created = models.DateTimeField(auto_now_add=True) # время создания
updated = models.DateTimeField(auto_now=True) # время редактирования
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft') # статус блога (например "опубликовано")
average_rating = models.FloatField(verbose_name=('Average rating'), default=0,)
objects = models.Manager() # Менеджер по умолчанию
published = PublishedManager() # Новый менеджер
tags = TaggableManager()
class Meta:
ordering = ('-publish',) # сортировка статей по дате публикация (по убыванию)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', args=[self.publish.year, self.publish.month, self.publish.day, self.slug])
def get_averages(self, max_value=None):
max_rating_value = 0
post_maximums = {}
post_averages = {}
posts = Post.objects.filter(counts_for_average=True, value__comment=self)
# Высчитываем среднее значение каждого поста
for post in posts:
post_average = None
ratings = Comment.objects.filter(comment=self, post=post, value__isnull=False).exclude(value='')
post_max = post_maximums[post]
for comment in comments:
if post_average is None:
post_average = float(comment.value)
else:
post_average += float(comment.value)
if post_average is not None:
post_average *= float(max_rating_value) / float(post_max)
post_averages[post] = (post_average / ratings.count())
total_average = 0
for post, post_average in post_averages.posts():
total_average += post_average
if not len(post_averages):
return (False, False)
total_average /= len(post_averages)
return total_average, post_averages
def get_average_rating(self, max_value=None):
total_average, post_averages = self.get_averages(max_value=max_value)
return total_average
def get_post_averages(self, max_value=None):
total_average, post_averages = self.get_averages(max_value=max_value)
return post_averages
class Comment(models.Model):
STATUS_CHOICES = (
('created', 'Created'),
('active', 'Active'),
)
rating_choices = DEFAULT_CHOICES
value = models.CharField(
max_length=20,
verbose_name=('Value'),
choices=rating_choices,
blank=True, null=True
)
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments') # делаем отношение "один ко многим", что бы в статье можно было оставлять несколько комментариев
name = models.CharField(max_length=80)
title = models.CharField(max_length=50)
body = models.TextField()
image = models.ImageField(upload_to='comments/images/', blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='active')
active = models.BooleanField(default=True) # Добавляем булевое поле, что бы можно было скрывать определенные комментарии
commented = CommentedManager()
class Meta:
ordering = ('created',)
def __str__(self):
return 'Comment by {} on {}'.format(self.name, self.value, self.post)
P.S. Сидел до ночи и все таки удалось решить эту задачу с помощью Numpy. Заняло это 3 строки, кому интересно:
def average_rating(self):
all_ratings = list(map(lambda x: x.value, self.comments.all()))
average = np.array(all_ratings).astype(np.float)
return np.average(average)
Так что теперь в админке у меня наконец то выводится средняя оценка Поста исходя из всех отзывов. Единственное что, пока не знаю как эту оценку потом вывести на главную страницу возле каждого поста)) И вообще интересно, как Вы оцениваете данное решение? Слишком ли я все сделал через жопу или все таки такой вариант имеет место быть? Всем заранее и по факту спасибо!)
Ответы (1 шт):
Самый производительный способ это создать новую модель, где будет сохранено количество отызвов в бд и сумма всех оценок. Т.е. при сохранении отзыва сумма всех оценок увеличивается на значение этой оценки, а количество записей на 1. Когда нужно вывести среднюю оценку нужно сумму всех оценок разделить на количество, это и будет среднее значение
Есть более просто способ, но менее производительный. Так же его можно использовать, если в бд уже есть записи, а таблица не была создана. Если интересен второй вариант, то напишите комментарий
Upd 2 способ:
# Пример функции, которая может быть использована для подсчёте
def calculate_average_rating():
# Сумма оценок
sum_of_raiting = 0
# Количество оценок
count = 0
# Находим все оценки от 1 до 5
for raiting in range(6):
# Получаем объекты оценок
objs = Comment.objects.filter(rating_choices=raiting)
# складываем кол-во оценок
count += objs.count()
# складываем сумму оценок
sum_of_raiting += objs.count() * i
# возвращаем среднюю оценку или None, если не удалось посчитать оценку
if count > 0:
return sum_of_raiting / count
else:
return None
P.S. У вас ошибка в models.py. Должно быть:
rating_choices = models.IntegerField(choices=DEFAULT_CHOICES)