lib.itmens/journal/models/mark.py
2024-07-01 18:05:19 -04:00

337 lines
12 KiB
Python

from datetime import datetime
from functools import cached_property
from typing import Any
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from loguru import logger
from catalog.models import Item, ItemCategory
from mastodon.models import get_spoiler_text
from takahe.utils import Takahe
from users.models import APIdentity, User
from .comment import Comment
from .note import Note
from .rating import Rating
from .review import Review
from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType
def share_mark(mark, post_as_new=False):
user = mark.owner.user
if not user or not user.mastodon:
return
stars = user.mastodon.rating_to_emoji(mark.rating_grade)
spoiler_text, txt = get_spoiler_text(mark.comment_text or "", mark.item)
content = f"{mark.get_action_for_feed()} {stars}\n{mark.item.absolute_url}\n{txt}{mark.tag_text}"
update_url = (
None if post_as_new else (mark.shelfmember.metadata or {}).get("shared_link")
)
response = user.mastodon.post(
content,
mark.visibility,
update_url,
spoiler_text,
)
if response is not None and response.status_code in [200, 201]:
j = response.json()
if "url" in j:
mark.shelfmember.metadata = {"shared_link": j["url"]}
mark.shelfmember.save(update_fields=["metadata"])
return True, 200
else:
logger.warning(response)
return False, response.status_code if response is not None else -1
class Mark:
"""
Holding Mark for an item on an shelf,
which is a combo object of ShelfMember, Comment, Rating and Tags.
it mimics previous mark behaviour.
"""
def __init__(self, owner: APIdentity, item: Item):
self.owner = owner
self.item = item
@cached_property
def shelfmember(self) -> ShelfMember | None:
return self.owner.shelf_manager.locate_item(self.item)
@property
def id(self) -> int | None:
return self.shelfmember.pk if self.shelfmember else None
@cached_property
def shelf(self) -> Shelf | None:
return self.shelfmember.parent if self.shelfmember else None
@property
def shelf_type(self) -> ShelfType | None:
return self.shelfmember.parent.shelf_type if self.shelfmember else None
@property
def action_label(self) -> str:
if self.shelfmember and self.shelf_type:
return ShelfManager.get_action_label(self.shelf_type, self.item.category)
if self.comment:
return ShelfManager.get_action_label(
ShelfType.PROGRESS, self.comment.item.category
)
return ""
@property
def status_label(self) -> str:
if self.shelfmember and self.shelf_type:
return ShelfManager.get_status_label(self.shelf_type, self.item.category)
if self.comment:
return ShelfManager.get_status_label(
ShelfType.PROGRESS, self.comment.item.category
)
return ""
@property
def action_label_for_feed(self) -> str:
return str(self.action_label)
def get_action_for_feed(self, item_link: str | None = None):
if self.shelfmember and self.shelf_type:
tpl = ShelfManager.get_action_template(self.shelf_type, self.item.category)
elif self.comment:
tpl = ShelfManager.get_action_template(
ShelfType.PROGRESS, self.comment.item.category
)
else:
tpl = ""
if item_link:
i = f'<a href="{item_link}">{self.item.display_title}</a>'
else:
i = self.item.display_title
return _(tpl).format(item=i)
@property
def shelf_label(self) -> str | None:
return (
ShelfManager.get_label(self.shelf_type, self.item.category)
if self.shelf_type
else None
)
@cached_property
def notes(self):
return Note.objects.filter(owner=self.owner, item=self.item).order_by(
"-created_time"
)
# post_ids = PiecePost.objects.filter(
# piece__note__owner_id=self.owner.pk, piece__note__item_id=self.item.pk
# ).values_list("post_id", flat=True)
# return Takahe.get_posts(list(post_ids))
@property
def created_time(self) -> datetime | None:
return self.shelfmember.created_time if self.shelfmember else None
@property
def metadata(self) -> dict[str, Any] | None:
return self.shelfmember.metadata if self.shelfmember else None
@property
def visibility(self) -> int:
if self.shelfmember:
return self.shelfmember.visibility
else:
# mark not created/saved yet, use user's default visibility
return self.owner.preference.default_visibility
@cached_property
def tags(self) -> list[str]:
return self.owner.tag_manager.get_item_tags(self.item)
@cached_property
def tag_text(self) -> str:
tags = [f"#{t}" for t in self.tags]
appending = self.owner.user.preference.mastodon_append_tag
if appending:
tags.append(appending)
tag_text = f"\n{' '.join(tags)}\n" if tags else ""
return tag_text
@cached_property
def rating(self):
return Rating.objects.filter(owner=self.owner, item=self.item).first()
@cached_property
def rating_grade(self) -> int | None:
return Rating.get_item_rating(self.item, self.owner)
@cached_property
def comment(self) -> Comment | None:
return Comment.objects.filter(owner=self.owner, item=self.item).first()
@property
def comment_text(self) -> str | None:
return (self.comment.text or None) if self.comment else None
@property
def comment_html(self) -> str | None:
return self.comment.html if self.comment else None
@cached_property
def review(self) -> Review | None:
return Review.objects.filter(owner=self.owner, item=self.item).first()
@property
def logs(self):
return ShelfLogEntry.objects.filter(owner=self.owner, item=self.item).order_by(
"timestamp"
)
"""
log entries
log entry will be created when item is added to shelf
log entry will be created when item is moved to another shelf
log entry will be created when item is removed from shelf (TODO change this to DEFERRED shelf)
timestamp of log entry will be updated whenever created_time of shelfmember is updated
any log entry can be deleted by user arbitrarily
posts
post will be created and set as current when item added to shelf
current post will be updated when comment or rating is updated
post will not be updated if only created_time is changed
post will be deleted, re-created and set as current if visibility changed
when item is moved to another shelf, a new post will be created
when item is removed from shelf, all post will be deleted
boost
post will be boosted to mastodon if user has mastodon token and site configured
"""
@property
def all_post_ids(self):
"""all post ids for this user and item"""
return self.logs.values_list("posts", flat=True)
@property
def current_post_ids(self):
"""all post ids for this user and item for its current status"""
return self.shelfmember.all_post_ids if self.shelfmember else []
@property
def latest_post_id(self):
"""latest post id for this user and item for its current status"""
return self.shelfmember.latest_post_id if self.shelfmember else None
def update(
self,
shelf_type: ShelfType | None,
comment_text: str | None = None,
rating_grade: int | None = None,
tags: list[str] | None = None,
visibility: int | None = None,
metadata: dict[str, Any] | None = None,
created_time: datetime | None = None,
share_to_mastodon: bool = False,
):
"""change shelf, comment or rating"""
if created_time and created_time >= timezone.now():
created_time = None
if visibility is None:
visibility = self.visibility
last_shelf_type = self.shelf_type
last_visibility = self.visibility if last_shelf_type else None
if tags is not None:
self.owner.tag_manager.tag_item(self.item, tags, visibility)
if shelf_type is None:
# take item off shelf
if self.shelfmember:
Takahe.delete_posts(self.shelfmember.all_post_ids)
self.shelfmember.log_and_delete()
if self.comment:
self.comment.delete()
if self.rating:
self.rating.delete()
return
# create/update shelf member and shelf log if necessary
if self.shelfmember and last_shelf_type == shelf_type:
shelfmember_changed = False
log_entry = self.shelfmember.ensure_log_entry()
if metadata is not None and metadata != self.shelfmember.metadata:
self.shelfmember.metadata = metadata
shelfmember_changed = True
if last_visibility != visibility:
self.shelfmember.visibility = visibility
shelfmember_changed = True
# retract most recent post about this status when visibility changed
if self.shelfmember.latest_post:
Takahe.delete_posts([self.shelfmember.latest_post.pk])
if created_time and created_time != self.shelfmember.created_time:
self.shelfmember.created_time = created_time
log_entry.timestamp = created_time
try:
log_entry.save(update_fields=["timestamp"])
except Exception:
log_entry.delete()
shelfmember_changed = True
if shelfmember_changed:
self.shelfmember.save()
else:
shelf = Shelf.objects.get(owner=self.owner, shelf_type=shelf_type)
d = {"parent": shelf, "visibility": visibility, "position": 0}
if metadata:
d["metadata"] = metadata
d["created_time"] = created_time or timezone.now()
self.shelfmember, _ = ShelfMember.objects.update_or_create(
owner=self.owner, item=self.item, defaults=d
)
self.shelfmember.ensure_log_entry()
self.shelfmember.clear_post_ids()
# create/update/detele comment if necessary
if comment_text is not None:
if comment_text != self.comment_text or visibility != last_visibility:
self.comment = Comment.comment_item(
self.item,
self.owner,
comment_text,
visibility,
self.shelfmember.created_time,
)
# create/update/detele rating if necessary
if rating_grade is not None:
if rating_grade != self.rating_grade or visibility != last_visibility:
Rating.update_item_rating(
self.item, self.owner, rating_grade, visibility
)
self.rating_grade = rating_grade
# publish a new or updated ActivityPub post
user: User = self.owner.user
post_as_new = shelf_type != last_shelf_type or visibility != last_visibility
classic_crosspost = user.preference.mastodon_repost_mode == 1
append = (
f"@{user.mastodon_acct}\n"
if visibility > 0 and share_to_mastodon and not classic_crosspost
else ""
)
post = Takahe.post_mark(self, post_as_new, append)
if post and self.item.category in (user.preference.auto_bookmark_cats or []):
if shelf_type == ShelfType.PROGRESS:
Takahe.bookmark(post.pk, self.owner.pk)
# async boost to mastodon
if post and share_to_mastodon:
if classic_crosspost:
share_mark(self, post_as_new)
elif user.mastodon:
user.mastodon.boost_later(post.url)
return True
def delete(self, keep_tags=False):
self.update(None, tags=None if keep_tags else [])
def delete_log(self, log_id: int):
ShelfLogEntry.objects.filter(
owner=self.owner, item=self.item, id=log_id
).delete()
def delete_all_logs(self):
self.logs.delete()