lib.itmens/journal/models/rating.py

178 lines
6.4 KiB
Python
Raw Normal View History

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
from django.db.models import Avg, Count
2025-03-08 12:28:26 -05:00
from catalog.models import Item, Performance, TVShow
2024-06-13 20:44:15 -04:00
from takahe.utils import Takahe
2023-07-20 21:59:49 -04:00
from users.models import APIdentity
from .common import Content
MIN_RATING_COUNT = 5
2025-03-08 12:28:26 -05:00
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,
"href": self.absolute_url,
2023-07-20 21:59:49 -04:00
}
@classmethod
2025-01-19 16:04:22 -05:00
def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
2025-02-03 17:20:35 -05:00
if post.local: # ignore local user updating their post via Mastodon API
return
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"],
2024-06-13 20:44:15 -04:00
"visibility": Takahe.visibility_t2n(post.visibility),
2023-07-20 21:59:49 -04:00
"created_time": datetime.fromisoformat(obj["published"]),
"edited_time": datetime.fromisoformat(obj["updated"]),
}
2024-04-06 00:13:50 -04:00
p = cls.objects.update_or_create(owner=owner, item=item, defaults=d)[0]
2024-06-13 20:44:15 -04:00
p.link_post_id(post.id)
2023-07-20 21:59:49 -04:00
return p
2025-03-08 12:28:26 -05:00
@classmethod
def get_info_for_item(cls, item: Item) -> dict:
stat = Rating.objects.filter(grade__isnull=False)
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
else:
stat = stat.filter(item=item)
stat = stat.values("grade").annotate(count=Count("grade"))
grades = [0] * 11
votes = 0
total = 0
for s in stat:
if s["grade"] and s["grade"] > 0 and s["grade"] < 11:
grades[s["grade"]] = s["count"]
total += s["count"] * s["grade"]
votes += s["count"]
if votes < MIN_RATING_COUNT:
return {"average": None, "count": votes, "distribution": [0] * 5}
else:
return {
"average": round(total / votes, 1),
"count": votes,
"distribution": [
100 * (grades[1] + grades[2]) // votes,
100 * (grades[3] + grades[4]) // votes,
100 * (grades[5] + grades[6]) // votes,
100 * (grades[7] + grades[8]) // votes,
100 * (grades[9] + grades[10]) // votes,
],
}
@staticmethod
def get_rating_for_item(item: Item) -> float | None:
stat = Rating.objects.filter(grade__isnull=False)
2025-03-08 12:28:26 -05:00
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
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)
2025-03-08 12:28:26 -05:00
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
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)
2025-03-08 12:28:26 -05:00
if item.__class__ in RATING_INCLUDES_CHILD_ITEMS:
stat = stat.filter(item_id__in=item.child_item_ids + [item.pk])
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,
):
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
@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()
return (rating.grade or None) if rating else None
2024-12-30 01:51:19 -05:00
def to_indexable_doc(self) -> dict[str, Any]:
# rating is not indexed individually but with shelfmember
return {}