2023-08-10 11:27:31 -04:00
|
|
|
import re
|
|
|
|
import uuid
|
2024-05-29 10:50:41 -04:00
|
|
|
from datetime import datetime
|
2023-08-10 11:27:31 -04:00
|
|
|
from functools import cached_property
|
2024-05-29 10:50:41 -04:00
|
|
|
from typing import Any
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
import django.dispatch
|
|
|
|
from django.conf import settings
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
from django.core.exceptions import PermissionDenied
|
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
|
|
|
from django.db import connection, models
|
|
|
|
from django.db.models import Avg, Count, Q
|
|
|
|
from django.utils import timezone
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
2023-07-20 21:59:49 -04:00
|
|
|
from loguru import logger
|
2023-08-10 11:27:31 -04:00
|
|
|
from markdownx.models import MarkdownxField
|
|
|
|
from polymorphic.models import PolymorphicModel
|
|
|
|
|
|
|
|
from catalog.collection.models import Collection as CatalogCollection
|
|
|
|
from catalog.common import jsondata
|
|
|
|
from catalog.common.models import Item, ItemCategory
|
|
|
|
from catalog.common.utils import DEFAULT_ITEM_COVER, piece_cover_path
|
2023-11-27 23:37:10 -05:00
|
|
|
from mastodon.api import boost_toot_later, share_mark
|
2023-07-20 21:59:49 -04:00
|
|
|
from takahe.utils import Takahe
|
|
|
|
from users.models import APIdentity
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
from .comment import Comment
|
2024-06-13 20:44:15 -04:00
|
|
|
from .note import Note
|
2023-08-10 11:27:31 -04:00
|
|
|
from .rating import Rating
|
|
|
|
from .review import Review
|
|
|
|
from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
def __init__(self, owner: APIdentity, item: Item):
|
|
|
|
self.owner = owner
|
2023-08-10 11:27:31 -04:00
|
|
|
self.item = item
|
|
|
|
|
|
|
|
@cached_property
|
2024-05-27 15:44:12 -04:00
|
|
|
def shelfmember(self) -> ShelfMember | None:
|
2023-08-10 11:27:31 -04:00
|
|
|
return self.owner.shelf_manager.locate_item(self.item)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def id(self) -> int | None:
|
2024-05-14 10:54:49 -04:00
|
|
|
return self.shelfmember.pk if self.shelfmember else None
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
@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:
|
2023-07-20 21:59:49 -04:00
|
|
|
if self.shelfmember and self.shelf_type:
|
2023-08-10 11:27:31 -04:00
|
|
|
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 ""
|
|
|
|
|
2024-05-29 10:50:41 -04:00
|
|
|
@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 ""
|
|
|
|
|
2024-01-20 22:52:08 -05:00
|
|
|
@property
|
|
|
|
def action_label_for_feed(self) -> str:
|
2024-03-24 22:28:22 -04:00
|
|
|
return str(self.action_label)
|
2024-01-20 22:52:08 -05:00
|
|
|
|
2024-05-29 10:50:41 -04:00
|
|
|
def get_action_for_feed(self, item_link: str | None = None):
|
2024-04-03 23:10:21 -04:00
|
|
|
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)
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
@property
|
|
|
|
def shelf_label(self) -> str | None:
|
|
|
|
return (
|
|
|
|
ShelfManager.get_label(self.shelf_type, self.item.category)
|
2023-07-20 21:59:49 -04:00
|
|
|
if self.shelf_type
|
2023-08-10 11:27:31 -04:00
|
|
|
else None
|
|
|
|
)
|
|
|
|
|
2024-06-13 20:44:15 -04:00
|
|
|
@cached_property
|
|
|
|
def notes(self):
|
2024-06-15 18:26:20 -04:00
|
|
|
return Note.objects.filter(owner=self.owner, item=self.item).order_by(
|
|
|
|
"-created_time"
|
|
|
|
)
|
2024-06-13 20:44:15 -04:00
|
|
|
# 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))
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
@property
|
2024-05-29 10:50:41 -04:00
|
|
|
def created_time(self) -> datetime | None:
|
2023-08-10 11:27:31 -04:00
|
|
|
return self.shelfmember.created_time if self.shelfmember else None
|
|
|
|
|
|
|
|
@property
|
2024-05-29 10:50:41 -04:00
|
|
|
def metadata(self) -> dict[str, Any] | None:
|
2023-08-10 11:27:31 -04:00
|
|
|
return self.shelfmember.metadata if self.shelfmember else None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def visibility(self) -> int:
|
2023-07-20 21:59:49 -04:00
|
|
|
if self.shelfmember:
|
|
|
|
return self.shelfmember.visibility
|
|
|
|
else:
|
2023-11-20 12:13:43 -05:00
|
|
|
# mark not created/saved yet, use user's default visibility
|
2023-08-20 21:46:53 +00:00
|
|
|
return self.owner.preference.default_visibility
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def tags(self) -> list[str]:
|
|
|
|
return self.owner.tag_manager.get_item_tags(self.item)
|
|
|
|
|
2024-06-04 11:06:12 -04:00
|
|
|
@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
|
|
|
|
|
2023-07-20 21:59:49 -04:00
|
|
|
@cached_property
|
|
|
|
def rating(self):
|
|
|
|
return Rating.objects.filter(owner=self.owner, item=self.item).first()
|
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
@cached_property
|
|
|
|
def rating_grade(self) -> int | None:
|
2023-07-20 21:59:49 -04:00
|
|
|
return Rating.get_item_rating(self.item, self.owner)
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
@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()
|
|
|
|
|
2023-11-20 01:59:26 -05:00
|
|
|
@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"""
|
2023-11-20 12:13:43 -05:00
|
|
|
return self.logs.values_list("posts", flat=True)
|
2023-11-20 01:59:26 -05:00
|
|
|
|
|
|
|
@property
|
|
|
|
def current_post_ids(self):
|
2023-11-20 12:13:43 -05:00
|
|
|
"""all post ids for this user and item for its current status"""
|
|
|
|
return self.shelfmember.all_post_ids if self.shelfmember else []
|
2023-11-20 01:59:26 -05:00
|
|
|
|
|
|
|
@property
|
|
|
|
def latest_post_id(self):
|
2023-11-20 12:13:43 -05:00
|
|
|
"""latest post id for this user and item for its current status"""
|
|
|
|
return self.shelfmember.latest_post_id if self.shelfmember else None
|
2023-11-20 01:59:26 -05:00
|
|
|
|
2023-08-10 11:27:31 -04:00
|
|
|
def update(
|
|
|
|
self,
|
2023-11-20 01:59:26 -05:00
|
|
|
shelf_type: ShelfType | None,
|
2023-11-20 12:13:43 -05:00
|
|
|
comment_text: str | None = None,
|
|
|
|
rating_grade: int | None = None,
|
2024-06-04 10:12:04 -04:00
|
|
|
tags: list[str] | None = None,
|
2023-11-20 12:13:43 -05:00
|
|
|
visibility: int | None = None,
|
2024-05-29 10:50:41 -04:00
|
|
|
metadata: dict[str, Any] | None = None,
|
|
|
|
created_time: datetime | None = None,
|
|
|
|
share_to_mastodon: bool = False,
|
2023-08-10 11:27:31 -04:00
|
|
|
):
|
2023-11-20 01:59:26 -05:00
|
|
|
"""change shelf, comment or rating"""
|
2023-08-10 11:27:31 -04:00
|
|
|
if created_time and created_time >= timezone.now():
|
|
|
|
created_time = None
|
2023-11-20 12:13:43 -05:00
|
|
|
if visibility is None:
|
|
|
|
visibility = self.visibility
|
2023-11-20 01:59:26 -05:00
|
|
|
last_shelf_type = self.shelf_type
|
|
|
|
last_visibility = self.visibility if last_shelf_type else None
|
2024-06-04 10:12:04 -04:00
|
|
|
if tags is not None:
|
|
|
|
self.owner.tag_manager.tag_item(self.item, tags, visibility)
|
|
|
|
if shelf_type is None:
|
2023-11-20 01:59:26 -05:00
|
|
|
# take item off shelf
|
2024-05-27 15:44:12 -04:00
|
|
|
if self.shelfmember:
|
2023-11-20 01:59:26 -05:00
|
|
|
Takahe.delete_posts(self.shelfmember.all_post_ids)
|
|
|
|
self.shelfmember.log_and_delete()
|
2023-12-15 20:41:18 -05:00
|
|
|
if self.comment:
|
|
|
|
self.comment.delete()
|
|
|
|
if self.rating:
|
|
|
|
self.rating.delete()
|
2023-11-20 01:59:26 -05:00
|
|
|
return
|
|
|
|
# create/update shelf member and shelf log if necessary
|
2024-05-27 15:44:12 -04:00
|
|
|
if self.shelfmember and last_shelf_type == shelf_type:
|
2023-11-20 01:59:26 -05:00
|
|
|
shelfmember_changed = False
|
2023-11-20 12:13:43 -05:00
|
|
|
log_entry = self.shelfmember.ensure_log_entry()
|
|
|
|
if metadata is not None and metadata != self.shelfmember.metadata:
|
|
|
|
self.shelfmember.metadata = metadata
|
|
|
|
shelfmember_changed = True
|
2023-11-20 01:59:26 -05:00
|
|
|
if last_visibility != visibility:
|
|
|
|
self.shelfmember.visibility = visibility
|
|
|
|
shelfmember_changed = True
|
|
|
|
# retract most recent post about this status when visibility changed
|
2023-11-22 22:29:38 -05:00
|
|
|
if self.shelfmember.latest_post:
|
|
|
|
Takahe.delete_posts([self.shelfmember.latest_post.pk])
|
2023-11-20 01:59:26 -05:00
|
|
|
if created_time and created_time != self.shelfmember.created_time:
|
|
|
|
self.shelfmember.created_time = created_time
|
|
|
|
log_entry.timestamp = created_time
|
2023-12-17 22:21:29 -05:00
|
|
|
try:
|
|
|
|
log_entry.save(update_fields=["timestamp"])
|
2024-04-06 00:13:50 -04:00
|
|
|
except Exception:
|
2023-12-17 22:21:29 -05:00
|
|
|
log_entry.delete()
|
2023-11-20 01:59:26 -05:00
|
|
|
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
|
2023-11-21 09:20:58 -05:00
|
|
|
d["created_time"] = created_time or timezone.now()
|
2023-11-20 01:59:26 -05:00
|
|
|
self.shelfmember, _ = ShelfMember.objects.update_or_create(
|
|
|
|
owner=self.owner, item=self.item, defaults=d
|
2023-08-10 11:27:31 -04:00
|
|
|
)
|
2023-11-20 12:13:43 -05:00
|
|
|
self.shelfmember.ensure_log_entry()
|
2023-11-20 01:59:26 -05:00
|
|
|
self.shelfmember.clear_post_ids()
|
|
|
|
# create/update/detele comment if necessary
|
2023-11-20 12:13:43 -05:00
|
|
|
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,
|
|
|
|
)
|
2023-11-20 01:59:26 -05:00
|
|
|
# create/update/detele rating if necessary
|
2023-11-20 12:13:43 -05:00
|
|
|
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
|
2023-11-20 01:59:26 -05:00
|
|
|
# publish a new or updated ActivityPub post
|
2024-06-15 23:38:33 -04:00
|
|
|
user = self.owner.user
|
2023-11-20 12:13:43 -05:00
|
|
|
post_as_new = shelf_type != last_shelf_type or visibility != last_visibility
|
2024-06-16 21:54:20 -04:00
|
|
|
classic_crosspost = user.preference.mastodon_repost_mode == 1
|
2023-12-09 23:22:03 -05:00
|
|
|
append = (
|
2024-06-15 23:38:33 -04:00
|
|
|
f"@{user.mastodon_acct}\n"
|
2024-06-16 21:54:20 -04:00
|
|
|
if visibility > 0 and share_to_mastodon and not classic_crosspost
|
2023-12-09 23:22:03 -05:00
|
|
|
else ""
|
|
|
|
)
|
|
|
|
post = Takahe.post_mark(self, post_as_new, append)
|
2024-06-15 23:38:33 -04:00
|
|
|
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)
|
2023-11-20 01:59:26 -05:00
|
|
|
# async boost to mastodon
|
|
|
|
if post and share_to_mastodon:
|
2024-06-16 21:54:20 -04:00
|
|
|
if classic_crosspost:
|
2023-12-15 20:53:27 -05:00
|
|
|
share_mark(self, post_as_new)
|
2023-11-27 23:37:10 -05:00
|
|
|
else:
|
2024-06-15 23:38:33 -04:00
|
|
|
boost_toot_later(user, post.url)
|
2023-11-20 01:59:26 -05:00
|
|
|
return True
|
2023-07-20 21:59:49 -04:00
|
|
|
|
2024-06-04 10:12:04 -04:00
|
|
|
def delete(self, keep_tags=False):
|
|
|
|
self.update(None, tags=None if keep_tags else [])
|
2023-08-10 11:27:31 -04:00
|
|
|
|
2024-05-29 10:50:41 -04:00
|
|
|
def delete_log(self, log_id: int):
|
2023-08-10 11:27:31 -04:00
|
|
|
ShelfLogEntry.objects.filter(
|
|
|
|
owner=self.owner, item=self.item, id=log_id
|
|
|
|
).delete()
|
|
|
|
|
|
|
|
def delete_all_logs(self):
|
|
|
|
self.logs.delete()
|