From baf678e4656f3d604e5287b69caa5797fc47f5bc Mon Sep 17 00:00:00 2001 From: Her Email Date: Mon, 20 Nov 2023 01:59:26 -0500 Subject: [PATCH] rewrite Mark.update() --- journal/api.py | 13 +- ...gentrypost_shelflogentry_posts_and_more.py | 59 ++++++ journal/models/common.py | 3 + journal/models/mark.py | 182 +++++++++++------- journal/models/shelf.py | 59 +++++- journal/views/mark.py | 14 +- journal/views/review.py | 14 +- mastodon/api.py | 8 + takahe/utils.py | 6 +- 9 files changed, 251 insertions(+), 107 deletions(-) create mode 100644 journal/migrations/0018_shelflogentrypost_shelflogentry_posts_and_more.py diff --git a/journal/api.py b/journal/api.py index ac0dd094..8fc6dd98 100644 --- a/journal/api.py +++ b/journal/api.py @@ -10,7 +10,7 @@ from catalog.common.models import * from common.api import * -from mastodon.api import boost_toot +from mastodon.api import boost_toot_later from .models import Mark, Review, ShelfType, TagManager, q_item_in_category @@ -195,7 +195,7 @@ def review_item(request, item_uuid: str, review: ReviewInSchema): item = Item.get_by_url(item_uuid) if not item: return 404, {"message": "Item not found"} - r, p = Review.update_item_review( + r, post = Review.update_item_review( item, request.user, review.title, @@ -203,13 +203,8 @@ def review_item(request, item_uuid: str, review: ReviewInSchema): review.visibility, created_time=review.created_time, ) - if p and review.post_to_fediverse and request.user.mastodon_username: - boost_toot( - request.user.mastodon_site, - request.user.mastodon_token, - p.url, - ) - + if post and review.post_to_fediverse: + boost_toot_later(request.user, post.url) return 200, {"message": "OK"} diff --git a/journal/migrations/0018_shelflogentrypost_shelflogentry_posts_and_more.py b/journal/migrations/0018_shelflogentrypost_shelflogentry_posts_and_more.py new file mode 100644 index 00000000..ac8face1 --- /dev/null +++ b/journal/migrations/0018_shelflogentrypost_shelflogentry_posts_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.7 on 2023-11-20 06:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("takahe", "0001_initial"), + ("journal", "0017_alter_piece_options_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ShelfLogEntryPost", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "log_entry", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="journal.shelflogentry", + ), + ), + ( + "post", + models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.CASCADE, + to="takahe.post", + ), + ), + ], + ), + migrations.AddField( + model_name="shelflogentry", + name="posts", + field=models.ManyToManyField( + related_name="log_entries", + through="journal.ShelfLogEntryPost", + to="takahe.post", + ), + ), + migrations.AddConstraint( + model_name="shelflogentrypost", + constraint=models.UniqueConstraint( + fields=("log_entry", "post"), name="unique_log_entry_post" + ), + ), + ] diff --git a/journal/models/common.py b/journal/models/common.py index c5024f62..5406daea 100644 --- a/journal/models/common.py +++ b/journal/models/common.py @@ -197,6 +197,9 @@ def link_post_id(self, post_id: int): def link_post(self, post: "Post"): return self.link_post_id(post.pk) + def clear_post_ids(self): + PiecePost.objects.filter(piece=self).delete() + @cached_property def latest_post(self): # local post id is ordered by their created time diff --git a/journal/models/mark.py b/journal/models/mark.py index 3657efc5..113b934f 100644 --- a/journal/models/mark.py +++ b/journal/models/mark.py @@ -20,7 +20,7 @@ from catalog.common import jsondata from catalog.common.models import Item, ItemCategory from catalog.common.utils import DEFAULT_ITEM_COVER, piece_cover_path -from mastodon.api import boost_toot +from mastodon.api import boost_toot_later from takahe.utils import Takahe from users.models import APIdentity @@ -119,86 +119,134 @@ def comment_html(self) -> str | None: 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""" + pass + + @property + def current_post_ids(self): + """all post ids for this user and item for its current shelf""" + pass + + @property + def latest_post_id(self): + """latest post id for this user and item for its current shelf""" + pass + + def wish(self): + """add to wishlist if not on shelf""" + if self.shelfmember: + logger.warning("item already on shelf, cannot wishlist again") + return False + self.shelfmember = ShelfMember.objects.create( + owner=self.owner, + item=self.item, + parent=Shelf.objects.get(owner=self.owner, shelf_type=ShelfType.WISHLIST), + visibility=self.owner.preference.default_visibility, + ) + self.shelfmember.create_log_entry() + post = Takahe.post_mark(self, True) + if post and not self.owner.preference.default_no_share: + boost_toot_later(self.owner, post.url) + return True + def update( self, - shelf_type, - comment_text, - rating_grade, - visibility, + shelf_type: ShelfType | None, + comment_text: str | None, + rating_grade: int | None, + visibility: int, metadata=None, created_time=None, share_to_mastodon=False, ): - post_to_feed = shelf_type is not None and ( - shelf_type != self.shelf_type - or comment_text != self.comment_text - or rating_grade != self.rating_grade - or visibility != self.visibility - ) - if shelf_type is None or visibility != self.visibility: - if self.shelfmember: - Takahe.delete_posts(self.shelfmember.all_post_ids) + """change shelf, comment or rating""" if created_time and created_time >= timezone.now(): created_time = None - post_as_new = shelf_type != self.shelf_type or visibility != self.visibility - original_visibility = self.visibility - if shelf_type != self.shelf_type or visibility != original_visibility: - self.shelfmember = self.owner.shelf_manager.move_item( - self.item, - shelf_type, - visibility=visibility, - metadata=metadata, + last_shelf_type = self.shelf_type + last_visibility = self.visibility if last_shelf_type else None + if shelf_type is None: # TODO change this use case to DEFERRED status + # take item off shelf + if last_shelf_type: + Takahe.delete_posts(self.shelfmember.all_post_ids) + self.shelfmember.log_and_delete() + return + # create/update shelf member and shelf log if necessary + if last_shelf_type == shelf_type: + shelfmember_changed = False + if last_visibility != visibility: + self.shelfmember.visibility = visibility + shelfmember_changed = True + # retract most recent post about this status when visibility changed + Takahe.delete_posts([self.shelfmember.latest_post_id]) + if created_time and created_time != self.shelfmember.created_time: + self.shelfmember.created_time = created_time + log_entry = self.shelfmember.ensure_log_entry() + log_entry.timestamp = created_time + log_entry.save(update_fields=["timestamp"]) + self.shelfmember.change_timestamp(created_time) + 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 + if created_time: + d["created_time"] = created_time + self.shelfmember, _ = ShelfMember.objects.update_or_create( + owner=self.owner, item=self.item, defaults=d ) - if self.shelfmember and created_time: - # if it's an update(not delete) and created_time is specified, - # update the timestamp of the shelfmember and log - log = ShelfLogEntry.objects.filter( - owner=self.owner, - item=self.item, - timestamp=self.shelfmember.created_time, - ).first() - self.shelfmember.created_time = created_time - self.shelfmember.save(update_fields=["created_time"]) - if log: - log.timestamp = created_time - log.save(update_fields=["timestamp"]) - else: - ShelfLogEntry.objects.create( - owner=self.owner, - shelf_type=shelf_type, - item=self.item, - metadata=self.metadata, - timestamp=created_time, - ) - if comment_text != self.comment_text or visibility != original_visibility: + self.shelfmember.create_log_entry() + self.shelfmember.clear_post_ids() + # create/update/detele comment if necessary + 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 if self.shelfmember else None, + self.shelfmember.created_time, ) - if rating_grade != self.rating_grade or visibility != original_visibility: + # create/update/detele rating if necessary + 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 - - post = Takahe.post_mark(self, post_as_new) if post_to_feed else None - if share_to_mastodon and post: - if ( - self.owner.user - and self.owner.user.mastodon_token - and self.owner.user.mastodon_site - ): - # TODO: make this a async task, given post to mastodon is slow and takahe post fanout may take time - if boost_toot( - self.owner.user.mastodon_site, - self.owner.user.mastodon_token, - post.url, - ): - return True - return False - else: - return True + # publish a new or updated ActivityPub post + post_as_new = shelf_type != self.shelf_type or visibility != self.visibility + post = Takahe.post_mark(self, post_as_new) + # async boost to mastodon + if post and share_to_mastodon: + boost_toot_later(self.owner, post.url) + return True def delete(self): # self.logs.delete() # When deleting a mark, all logs of the mark are deleted first. @@ -211,9 +259,3 @@ def delete_log(self, log_id): def delete_all_logs(self): self.logs.delete() - - @property - def logs(self): - return ShelfLogEntry.objects.filter(owner=self.owner, item=self.item).order_by( - "timestamp" - ) diff --git a/journal/models/shelf.py b/journal/models/shelf.py index fd6abb9a..a9fd8b56 100644 --- a/journal/models/shelf.py +++ b/journal/models/shelf.py @@ -8,7 +8,7 @@ from loguru import logger from catalog.models import Item, ItemCategory -from takahe.models import Identity +from takahe.models import Identity, Post from users.models import APIdentity from .common import q_item_in_category @@ -92,7 +92,6 @@ def update_by_ap_object( "parent": shelf, "position": 0, "local": False, - # "remote_id": obj["id"], "visibility": visibility, "created_time": datetime.fromisoformat(obj["published"]), "edited_time": datetime.fromisoformat(obj["updated"]), @@ -129,6 +128,38 @@ def comment_text(self): def tags(self): return self.mark.tags + def get_log_entry(self): + return ShelfLogEntry.objects.filter( + owner=self.owner, + item=self.item, + timestamp=self.created_time, + ).first() + + def create_log_entry(self): + return ShelfLogEntry.objects.create( + owner=self.owner, + shelf_type=self.shelf_type, + item=self.item, + metadata=self.metadata, + timestamp=self.created_time, + ) + + def ensure_log_entry(self): + return self.get_log_entry() or self.create_log_entry() + + def log_and_delete(self): + ShelfLogEntry.objects.create( + owner=self.owner, + shelf_type=None, + item=self.item, + timestamp=timezone.now(), + ) + self.delete() + + def link_post_id(self, post_id: int): + self.ensure_log_entry().link_post_id(post_id) + return super().link_post_id(post_id) + class Shelf(List): """ @@ -153,9 +184,12 @@ class ShelfLogEntry(models.Model): 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) + metadata = models.JSONField(default=dict) # TODO Remove this field created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) + posts = models.ManyToManyField( + "takahe.Post", related_name="log_entries", through="ShelfLogEntryPost" + ) def __str__(self): return f"{self.owner}:{self.shelf_type}:{self.item.uuid}:{self.timestamp}:{self.metadata}" @@ -167,6 +201,23 @@ def action_label(self): else: return _("移除标记") + def link_post_id(self, post_id: int): + ShelfLogEntryPost.objects.get_or_create(log_entry=self, post_id=post_id) + + +class ShelfLogEntryPost(models.Model): + log_entry = models.ForeignKey(ShelfLogEntry, on_delete=models.CASCADE) + post = models.ForeignKey( + "takahe.Post", db_constraint=False, db_index=True, on_delete=models.CASCADE + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["log_entry", "post"], name="unique_log_entry_post" + ), + ] + class ShelfManager: """ @@ -190,7 +241,7 @@ def initialize(self): def locate_item(self, item: Item) -> ShelfMember | None: return ShelfMember.objects.filter(item=item, owner=self.owner).first() - def move_item( + def move_item( # TODO remove this method self, item: Item, shelf_type: ShelfType, diff --git a/journal/views/mark.py b/journal/views/mark.py index f5821ece..8b44ff1d 100644 --- a/journal/views/mark.py +++ b/journal/views/mark.py @@ -13,7 +13,7 @@ from catalog.models import * from common.utils import AuthedHttpRequest, PageLinksGenerator, get_uuid_or_404 -from mastodon.api import boost_toot +from mastodon.api import boost_toot_later from takahe.utils import Takahe from ..models import Comment, Mark, Piece, ShelfType, ShelfTypeNames, TagManager @@ -32,7 +32,7 @@ def wish(request: AuthedHttpRequest, item_uuid): item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) if not item: raise Http404() - request.user.identity.shelf_manager.move_item(item, ShelfType.WISHLIST) + Mark(request.user.identity, item).wish() if request.GET.get("back"): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) return HttpResponse(_checkmark) @@ -221,14 +221,8 @@ def comment(request: AuthedHttpRequest, item_uuid): ) post = Takahe.post_comment(comment, False) share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False)) - if post and share_to_mastodon and request.user.mastodon_username: - boost_toot( - request.user.mastodon_site, - request.user.mastodon_token, - post.url, - ) - # if post_error: - # return render_relogin(request) + if post and share_to_mastodon: + boost_toot_later(request.user, post.url) return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) raise BadRequest() diff --git a/journal/views/review.py b/journal/views/review.py index f68d0b21..137b1f99 100644 --- a/journal/views/review.py +++ b/journal/views/review.py @@ -14,7 +14,7 @@ from catalog.models import * from common.utils import AuthedHttpRequest, PageLinksGenerator, get_uuid_or_404 from journal.models.renderers import convert_leading_space_in_md, render_md -from mastodon.api import boost_toot +from mastodon.api import boost_toot_later from users.models import User from users.models.apidentity import APIdentity @@ -84,16 +84,8 @@ def review_edit(request: AuthedHttpRequest, item_uuid, review_uuid=None): ) if not review: raise BadRequest() - if ( - form.cleaned_data["share_to_mastodon"] - and request.user.mastodon_username - and post - ): - boost_toot( - request.user.mastodon_site, - request.user.mastodon_token, - post.url, - ) + if form.cleaned_data["share_to_mastodon"] and post: + boost_toot_later(request.user, post.url) return redirect(reverse("journal:review_retrieve", args=[review.uuid])) else: raise BadRequest() diff --git a/mastodon/api.py b/mastodon/api.py index bf260349..e378272a 100644 --- a/mastodon/api.py +++ b/mastodon/api.py @@ -5,6 +5,7 @@ import string from urllib.parse import quote +import django_rq import requests from django.conf import settings from loguru import logger @@ -117,6 +118,13 @@ def boost_toot(site, token, toot_url): return None +def boost_toot_later(user, post_url): + if user and user.mastodon_token and user.mastodon_site and post_url: + django_rq.get_queue("fetch").enqueue( + boost_toot, user.mastodon_site, user.mastodon_token, post_url + ) + + def post_toot( site, content, diff --git a/takahe/utils.py b/takahe/utils.py index 4fbc543e..7af1bece 100644 --- a/takahe/utils.py +++ b/takahe/utils.py @@ -438,7 +438,7 @@ def post_comment(comment, share_as_new_post: bool) -> Post | None: ) if not post: return - comment.link_post(post) + comment.link_post_id(post.pk) return post @staticmethod @@ -485,7 +485,7 @@ def post_review(review, share_as_new_post: bool) -> Post | None: ) if not post: return - review.link_post(post) + review.link_post_id(post.pk) return post @staticmethod @@ -540,7 +540,7 @@ def post_mark(mark, share_as_new_post: bool) -> Post | None: return for piece in [mark.shelfmember, mark.comment, mark.rating]: if piece: - piece.link_post(post) + piece.link_post_id(post.pk) return post @staticmethod