lib.itmens/journal/models/shelf.py
2023-10-22 17:46:34 -04:00

325 lines
12 KiB
Python

from datetime import datetime
from functools import cached_property
from typing import TYPE_CHECKING
from django.db import connection, models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from loguru import logger
from catalog.models import Item, ItemCategory
from takahe.models import Identity
from users.models import APIdentity
from .common import q_item_in_category
from .itemlist import List, ListMember
if TYPE_CHECKING:
from .mark import Mark
class ShelfType(models.TextChoices):
WISHLIST = ("wishlist", "未开始")
PROGRESS = ("progress", "进行中")
COMPLETE = ("complete", "完成")
# DISCARDED = ('discarded', '放弃')
ShelfTypeNames = [
[ItemCategory.Book, ShelfType.WISHLIST, _("想读")],
[ItemCategory.Book, ShelfType.PROGRESS, _("在读")],
[ItemCategory.Book, ShelfType.COMPLETE, _("读过")],
[ItemCategory.Movie, ShelfType.WISHLIST, _("想看")],
[ItemCategory.Movie, ShelfType.PROGRESS, _("在看")],
[ItemCategory.Movie, ShelfType.COMPLETE, _("看过")],
[ItemCategory.TV, ShelfType.WISHLIST, _("想看")],
[ItemCategory.TV, ShelfType.PROGRESS, _("在看")],
[ItemCategory.TV, ShelfType.COMPLETE, _("看过")],
[ItemCategory.Music, ShelfType.WISHLIST, _("想听")],
[ItemCategory.Music, ShelfType.PROGRESS, _("在听")],
[ItemCategory.Music, ShelfType.COMPLETE, _("听过")],
[ItemCategory.Game, ShelfType.WISHLIST, _("想玩")],
[ItemCategory.Game, ShelfType.PROGRESS, _("在玩")],
[ItemCategory.Game, ShelfType.COMPLETE, _("玩过")],
[ItemCategory.Podcast, ShelfType.WISHLIST, _("想听")],
[ItemCategory.Podcast, ShelfType.PROGRESS, _("在听")],
[ItemCategory.Podcast, ShelfType.COMPLETE, _("听过")],
# disable all shelves for PodcastEpisode
[ItemCategory.Performance, ShelfType.WISHLIST, _("想看")],
# disable progress shelf for Performance
[ItemCategory.Performance, ShelfType.PROGRESS, _("")],
[ItemCategory.Performance, ShelfType.COMPLETE, _("看过")],
]
class ShelfMember(ListMember):
parent = models.ForeignKey(
"Shelf", related_name="members", on_delete=models.CASCADE
)
class Meta:
unique_together = [["owner", "item"]]
indexes = [
models.Index(fields=["parent_id", "visibility", "created_time"]),
]
@property
def ap_object(self):
return {
"id": self.absolute_url,
"type": "Status",
"status": self.parent.shelf_type,
"published": self.created_time.isoformat(),
"updated": self.edited_time.isoformat(),
"attributedTo": self.owner.actor_uri,
"relatedWith": self.item.absolute_url,
"url": self.absolute_url,
}
@classmethod
def update_by_ap_object(
cls, owner: APIdentity, item: Identity, obj: dict, post_id: int, visibility: int
):
if not obj:
cls.objects.filter(owner=owner, item=item).delete()
return
shelf = owner.shelf_manager.get_shelf(obj["status"])
if not shelf:
logger.warning(f"unable to locate shelf for {owner}, {obj}")
return
d = {
"parent": shelf,
"position": 0,
"local": False,
# "remote_id": obj["id"],
"post_id": post_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)
return p
@cached_property
def mark(self) -> "Mark":
from .mark import Mark
m = Mark(self.owner, self.item)
m.shelfmember = self
return m
@property
def shelf_label(self) -> str | None:
return ShelfManager.get_label(self.parent.shelf_type, self.item.category)
@property
def shelf_type(self):
return self.parent.shelf_type
@property
def rating_grade(self):
return self.mark.rating_grade
@property
def comment_text(self):
return self.mark.comment_text
@property
def tags(self):
return self.mark.tags
class Shelf(List):
"""
Shelf
"""
class Meta:
unique_together = [["owner", "shelf_type"]]
MEMBER_CLASS = ShelfMember
items = models.ManyToManyField(Item, through="ShelfMember", related_name="+")
shelf_type = models.CharField(
choices=ShelfType.choices, max_length=100, null=False, blank=False
)
def __str__(self):
return f"{self.id} [{self.owner} {self.shelf_type} list]"
class ShelfLogEntry(models.Model):
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
shelf_type = models.CharField(choices=ShelfType.choices, max_length=100, null=True)
item = models.ForeignKey(Item, on_delete=models.PROTECT)
timestamp = models.DateTimeField() # this may later be changed by user
metadata = models.JSONField(default=dict)
created_time = models.DateTimeField(auto_now_add=True)
edited_time = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.owner}:{self.shelf_type}:{self.item.uuid}:{self.timestamp}:{self.metadata}"
@property
def action_label(self):
if self.shelf_type:
return ShelfManager.get_action_label(self.shelf_type, self.item.category)
else:
return _("移除标记")
class ShelfManager:
"""
ShelfManager
all shelf operations should go thru this class so that ShelfLogEntry can be properly populated
ShelfLogEntry can later be modified if user wish to change history
"""
def __init__(self, owner):
self.owner = owner
qs = Shelf.objects.filter(owner=self.owner)
self.shelf_list = {v.shelf_type: v for v in qs}
if len(self.shelf_list) == 0:
self.initialize()
def initialize(self):
for qt in ShelfType:
self.shelf_list[qt] = Shelf.objects.create(owner=self.owner, shelf_type=qt)
def locate_item(self, item: Item) -> ShelfMember | None:
return ShelfMember.objects.filter(item=item, owner=self.owner).first()
def move_item(
self,
item: Item,
shelf_type: ShelfType,
visibility: int = 0,
metadata: dict | None = None,
):
# shelf_type=None means remove from current shelf
# metadata=None means no change
if not item:
raise ValueError("empty item")
new_shelfmember = None
last_shelfmember = self.locate_item(item)
last_shelf = last_shelfmember.parent if last_shelfmember else None
last_metadata = last_shelfmember.metadata if last_shelfmember else None
last_visibility = last_shelfmember.visibility if last_shelfmember else None
shelf = self.shelf_list[shelf_type] if shelf_type else None
changed = False
if last_shelf != shelf: # change shelf
changed = True
if last_shelf:
last_shelf.remove_item(item)
if shelf:
new_shelfmember = shelf.append_item(
item, visibility=visibility, metadata=metadata or {}
)
elif last_shelf is None:
raise ValueError("empty shelf")
else:
new_shelfmember = last_shelfmember
if last_shelfmember:
if (
metadata is not None and metadata != last_metadata
): # change metadata
changed = True
last_shelfmember.metadata = metadata
last_shelfmember.visibility = visibility
last_shelfmember.save()
elif visibility != last_visibility: # change visibility
last_shelfmember.visibility = visibility
last_shelfmember.save()
if changed:
if metadata is None:
metadata = last_metadata or {}
log_time = (
new_shelfmember.created_time
if new_shelfmember and new_shelfmember != last_shelfmember
else timezone.now()
)
ShelfLogEntry.objects.create(
owner=self.owner,
shelf_type=shelf_type,
item=item,
metadata=metadata,
timestamp=log_time,
)
return new_shelfmember
def get_log(self):
return ShelfLogEntry.objects.filter(owner=self.owner).order_by("timestamp")
def get_log_for_item(self, item: Item):
return ShelfLogEntry.objects.filter(owner=self.owner, item=item).order_by(
"timestamp"
)
def get_shelf(self, shelf_type: ShelfType):
return self.shelf_list[shelf_type]
def get_latest_members(
self, shelf_type: ShelfType, item_category: ItemCategory | None = None
):
qs = self.shelf_list[shelf_type].members.all().order_by("-created_time")
if item_category:
return qs.filter(q_item_in_category(item_category))
else:
return qs
# def get_items_on_shelf(self, item_category, shelf_type):
# shelf = (
# self.owner.shelf_set.all()
# .filter(item_category=item_category, shelf_type=shelf_type)
# .first()
# )
# return shelf.members.all().order_by
@classmethod
def get_action_label(
cls, shelf_type: ShelfType, item_category: ItemCategory
) -> str:
sts = [
n[2] for n in ShelfTypeNames if n[0] == item_category and n[1] == shelf_type
]
return sts[0] if sts else str(shelf_type)
@classmethod
def get_label(cls, shelf_type: ShelfType, item_category: ItemCategory):
ic = ItemCategory(item_category).label
st = cls.get_action_label(shelf_type, item_category)
return (
_("{shelf_label}{item_category}").format(shelf_label=st, item_category=ic)
if st
else None
)
@staticmethod
def get_manager_for_user(owner: APIdentity):
return ShelfManager(owner)
def get_calendar_data(self, max_visiblity: int):
shelf_id = self.get_shelf(ShelfType.COMPLETE).pk
timezone_offset = timezone.localtime(timezone.now()).strftime("%z")
timezone_offset = timezone_offset[: len(timezone_offset) - 2]
calendar_data = {}
sql = "SELECT to_char(DATE(journal_shelfmember.created_time::timestamp AT TIME ZONE %s), 'YYYY-MM-DD') AS dat, django_content_type.model typ, COUNT(1) count FROM journal_shelfmember, catalog_item, django_content_type WHERE journal_shelfmember.item_id = catalog_item.id AND django_content_type.id = catalog_item.polymorphic_ctype_id AND parent_id = %s AND journal_shelfmember.created_time >= NOW() - INTERVAL '366 days' AND journal_shelfmember.visibility <= %s GROUP BY item_id, dat, typ;"
with connection.cursor() as cursor:
cursor.execute(sql, [timezone_offset, shelf_id, int(max_visiblity)])
data = cursor.fetchall()
for line in data:
date = line[0]
typ = line[1]
if date not in calendar_data:
calendar_data[date] = {"items": []}
if typ[:2] == "tv":
typ = "movie"
elif typ == "album":
typ = "music"
elif typ == "edition":
typ = "book"
elif typ not in ["book", "movie", "music", "game"]:
typ = "other"
if typ not in calendar_data[date]["items"]:
calendar_data[date]["items"].append(typ)
return calendar_data