2023-07-20 21:59:49 -04:00
|
|
|
from datetime import datetime
|
2023-12-30 22:20:15 -05:00
|
|
|
from typing import Any
|
2023-07-20 21:59:49 -04:00
|
|
|
|
2023-12-30 22:20:15 -05:00
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
|
|
from django.db import models
|
2023-08-10 11:27:31 -04:00
|
|
|
from django.db.models import Avg, Count, Q
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
2023-12-30 22:20:15 -05:00
|
|
|
from catalog.models import Item
|
2023-07-20 21:59:49 -04:00
|
|
|
from users.models import APIdentity
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
from .common import Content
|
|
|
|
|
|
|
|
MIN_RATING_COUNT = 5
|
|
|
|
RATING_INCLUDES_CHILD_ITEMS = ["tvshow", "performance"]
|
|
|
|
|
|
|
|
|
|
|
|
class Rating(Content):
|
|
|
|
class Meta:
|
|
|
|
unique_together = [["owner", "item"]]
|
|
|
|
|
|
|
|
grade = models.PositiveSmallIntegerField(
|
|
|
|
default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True
|
|
|
|
)
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
@property
|
|
|
|
def ap_object(self):
|
|
|
|
return {
|
|
|
|
"id": self.absolute_url,
|
|
|
|
"type": "Rating",
|
|
|
|
"best": 10,
|
|
|
|
"worst": 1,
|
|
|
|
"value": self.grade,
|
|
|
|
"published": self.created_time.isoformat(),
|
|
|
|
"updated": self.edited_time.isoformat(),
|
|
|
|
"attributedTo": self.owner.actor_uri,
|
2023-11-28 08:45:04 -05:00
|
|
|
"withRegardTo": self.item.absolute_url,
|
2023-11-17 22:46:31 -05:00
|
|
|
"href": self.absolute_url,
|
2023-07-20 21:59:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
2023-11-20 19:11:02 -05:00
|
|
|
p = cls.objects.filter(owner=owner, item=item).first()
|
|
|
|
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
|
|
|
return p # incoming ap object is older than what we have, no update needed
|
2023-07-20 21:59:49 -04:00
|
|
|
value = obj.get("value", 0) if obj else 0
|
|
|
|
if not value:
|
|
|
|
cls.objects.filter(owner=owner, item=item).delete()
|
|
|
|
return
|
|
|
|
best = obj.get("best", 5)
|
|
|
|
worst = obj.get("worst", 1)
|
|
|
|
if best <= worst:
|
|
|
|
return
|
|
|
|
if value < worst:
|
|
|
|
value = worst
|
|
|
|
if value > best:
|
|
|
|
value = best
|
|
|
|
if best != 10 or worst != 1:
|
|
|
|
value = round(9 * (value - worst) / (best - worst)) + 1
|
|
|
|
else:
|
|
|
|
value = round(value)
|
|
|
|
d = {
|
|
|
|
"grade": value,
|
|
|
|
"local": False,
|
|
|
|
"remote_id": obj["id"],
|
|
|
|
"visibility": visibility,
|
|
|
|
"created_time": datetime.fromisoformat(obj["published"]),
|
|
|
|
"edited_time": datetime.fromisoformat(obj["updated"]),
|
|
|
|
}
|
|
|
|
p, _ = cls.objects.update_or_create(owner=owner, item=item, defaults=d)
|
2023-08-26 01:27:18 +00:00
|
|
|
p.link_post_id(post_id)
|
2023-07-20 21:59:49 -04:00
|
|
|
return p
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
@staticmethod
|
|
|
|
def get_rating_for_item(item: Item) -> float | None:
|
|
|
|
stat = Rating.objects.filter(grade__isnull=False)
|
|
|
|
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
|
|
|
|
stat = stat.filter(item_id__in=item.child_item_ids + [item.id])
|
|
|
|
else:
|
|
|
|
stat = stat.filter(item=item)
|
|
|
|
stat = stat.aggregate(average=Avg("grade"), count=Count("item"))
|
|
|
|
return round(stat["average"], 1) if stat["count"] >= MIN_RATING_COUNT else None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_rating_count_for_item(item: Item) -> int:
|
|
|
|
stat = Rating.objects.filter(grade__isnull=False)
|
|
|
|
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
|
|
|
|
stat = stat.filter(item_id__in=item.child_item_ids + [item.id])
|
|
|
|
else:
|
|
|
|
stat = stat.filter(item=item)
|
|
|
|
stat = stat.aggregate(count=Count("item"))
|
|
|
|
return stat["count"]
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_rating_distribution_for_item(item: Item):
|
|
|
|
stat = Rating.objects.filter(grade__isnull=False)
|
|
|
|
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
|
|
|
|
stat = stat.filter(item_id__in=item.child_item_ids + [item.id])
|
|
|
|
else:
|
|
|
|
stat = stat.filter(item=item)
|
|
|
|
stat = stat.values("grade").annotate(count=Count("grade")).order_by("grade")
|
|
|
|
g = [0] * 11
|
|
|
|
t = 0
|
|
|
|
for s in stat:
|
|
|
|
g[s["grade"]] = s["count"]
|
|
|
|
t += s["count"]
|
|
|
|
if t < MIN_RATING_COUNT:
|
|
|
|
return [0] * 5
|
|
|
|
r = [
|
|
|
|
100 * (g[1] + g[2]) // t,
|
|
|
|
100 * (g[3] + g[4]) // t,
|
|
|
|
100 * (g[5] + g[6]) // t,
|
|
|
|
100 * (g[7] + g[8]) // t,
|
|
|
|
100 * (g[9] + g[10]) // t,
|
|
|
|
]
|
|
|
|
return r
|
|
|
|
|
|
|
|
@staticmethod
|
2023-07-20 21:59:49 -04:00
|
|
|
def update_item_rating(
|
2023-12-30 22:20:15 -05:00
|
|
|
item: Item,
|
|
|
|
owner: APIdentity,
|
|
|
|
rating_grade: int | None,
|
|
|
|
visibility: int = 0,
|
|
|
|
created_time: datetime | None = None,
|
2023-08-10 11:27:31 -04:00
|
|
|
):
|
|
|
|
if rating_grade and (rating_grade < 1 or rating_grade > 10):
|
|
|
|
raise ValueError(f"Invalid rating grade: {rating_grade}")
|
|
|
|
if not rating_grade:
|
2023-12-30 22:20:15 -05:00
|
|
|
Rating.objects.filter(owner=owner, item=item).delete()
|
|
|
|
else:
|
|
|
|
d: dict[str, Any] = {"grade": rating_grade, "visibility": visibility}
|
|
|
|
if created_time:
|
|
|
|
d["created_time"] = created_time
|
|
|
|
r, _ = Rating.objects.update_or_create(owner=owner, item=item, defaults=d)
|
|
|
|
return r
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
@staticmethod
|
2023-07-20 21:59:49 -04:00
|
|
|
def get_item_rating(item: Item, owner: APIdentity) -> int | None:
|
|
|
|
rating = Rating.objects.filter(owner=owner, item=item).first()
|
2023-08-10 11:27:31 -04:00
|
|
|
return (rating.grade or None) if rating else None
|