enable multiple posts associate with one piece

This commit is contained in:
Your Name 2023-08-26 01:27:18 +00:00 committed by Henri Dickson
parent a761eaca19
commit 4cd1f6b10d
15 changed files with 188 additions and 64 deletions

View file

@ -30,6 +30,9 @@ RUN --mount=type=cache,sharing=locked,target=/var/cache/apt-run apt-get update \
gettext-base
RUN busybox --install
# postgresql and redis cli are not required, but install for development convenience
RUN --mount=type=cache,sharing=locked,target=/var/cache/apt-run apt-get install -y --no-install-recommends postgresql-client redis-tools
COPY . /neodb
WORKDIR /neodb
COPY --from=build /neodb-venv /neodb-venv

View file

@ -46,10 +46,10 @@
data-uuid="{{ comment.item.uuid }}"><i class="fa-regular fa-circle-play"></i></a>
</span>
{% endif %}
{% if comment.post %}
{% include "action_reply_post.html" with post=comment.post %}
{% include "action_like_post.html" with post=comment.post %}
{% include "action_open_post.html" with post=comment.post %}
{% if comment.latest_post %}
{% include "action_reply_piece.html" with post=comment.latest_post piece=comment %}
{% include "action_like_post.html" with post=comment.latest_post %}
{% include "action_open_post.html" with post=comment.latest_post %}
{% endif %}
</span>
<span>
@ -66,7 +66,7 @@
</span>
{% if comment.item != item %}<a href="{{ comment.item_url }}">{{ comment.item.title }}</a>{% endif %}
<div>{{ comment.html|safe }}</div>
{% if comment.post_id %}<div id="replies_{{ comment.post_id }}"></div>{% endif %}
{% if comment.latest_post %}<div id="replies_{{ comment.latest_post.pk }}"></div>{% endif %}
</section>
{% else %}
<a hx-get="{% url 'catalog:comments' comment.item.url_path comment.item.uuid %}?last={{ comment.created_time|date:'Y-m-d H:i:s.uO'|urlencode }}"

View file

@ -0,0 +1,56 @@
# Generated by Django 4.2.4 on 2023-08-25 18:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("takahe", "0001_initial"),
("journal", "0015_use_identity_support_remote_piece"),
]
operations = [
migrations.CreateModel(
name="PiecePost",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"piece",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="journal.piece"
),
),
(
"post",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.CASCADE,
to="takahe.post",
),
),
],
),
migrations.AddField(
model_name="piece",
name="posts",
field=models.ManyToManyField(
related_name="pieces", through="journal.PiecePost", to="takahe.post"
),
),
migrations.AddConstraint(
model_name="piecepost",
constraint=models.UniqueConstraint(
fields=("piece", "post"), name="unique_piece_post"
),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 4.2.4 on 2023-08-26 00:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("journal", "0016_piecepost_piece_posts_piecepost_unique_piece_post"),
]
operations = [
migrations.AlterModelOptions(
name="piece",
options={"base_manager_name": "objects"},
),
migrations.RemoveIndex(
model_name="piece",
name="journal_pie_post_id_6a74ff_idx",
),
migrations.RemoveField(
model_name="piece",
name="post_id",
),
]

View file

@ -38,12 +38,12 @@ class Comment(Content):
"text": content,
"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)
p.link_post_id(post_id)
return p
@property

View file

@ -1,5 +1,7 @@
import re
import uuid
from functools import cached_property
from typing import TYPE_CHECKING
from django.conf import settings
from django.db import connection, models
@ -10,13 +12,14 @@ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
from catalog.common.models import AvailableItemCategory, Item, ItemCategory
from catalog.models import *
from catalog.models import item_categories, item_content_types
from takahe.utils import Takahe
from users.models import APIdentity, User
from .mixins import UserOwnedObjectMixin
_logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from takahe.models import Post
class VisibilityType(models.IntegerChoices):
@ -117,12 +120,9 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
url_path = "p" # subclass must specify this
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
local = models.BooleanField(default=True)
post_id = models.BigIntegerField(null=True, default=None)
class Meta:
indexes = [
models.Index(fields=["post_id"]),
]
posts = models.ManyToManyField(
"takahe.Post", related_name="pieces", through="PiecePost"
)
@property
def uuid(self):
@ -140,32 +140,32 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
def api_url(self):
return f"/api/{self.url}" if self.url_path else None
@property
def post(self):
return Takahe.get_post(self.post_id) if self.post_id else None
@property
def shared_link(self):
return Takahe.get_post_url(self.post_id) if self.post_id else None
return Takahe.get_post_url(self.latest_post.pk) if self.latest_post else None
@property
def like_count(self):
return (
Takahe.get_post_stats(self.post_id).get("likes", 0) if self.post_id else 0
Takahe.get_post_stats(self.latest_post.pk).get("likes", 0)
if self.latest_post
else 0
)
def is_liked_by(self, user):
return self.post_id and Takahe.post_liked_by(self.post_id, user)
return self.latest_post and Takahe.post_liked_by(self.latest_post.pk, user)
@property
def reply_count(self):
return (
Takahe.get_post_stats(self.post_id).get("replies", 0) if self.post_id else 0
Takahe.get_post_stats(self.latest_post.pk).get("replies", 0)
if self.latest_post
else 0
)
def get_replies(self, viewing_identity):
return Takahe.get_post_replies(
self.post_id, viewing_identity.pk if viewing_identity else None
return Takahe.get_replies_for_posts(
self.all_post_ids, viewing_identity.pk if viewing_identity else None
)
@classmethod
@ -189,6 +189,37 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
def ap_object(self):
raise NotImplemented
def link_post_id(self, post_id: int):
PiecePost.objects.get_or_create(piece=self, post_id=post_id)
def link_post(self, post: "Post"):
return self.link_post_id(post.pk)
@cached_property
def latest_post(self):
# local post id is ordered by their created time
pp = PiecePost.objects.filter(piece=self).order_by("-post_id").first()
return Takahe.get_post(pp.post_id) if pp else None # type: ignore
@cached_property
def all_post_ids(self):
post_ids = list(
PiecePost.objects.filter(piece=self).values_list("post_id", flat=True)
)
return post_ids
class PiecePost(models.Model):
piece = models.ForeignKey(Piece, 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=["piece", "post"], name="unique_piece_post"),
]
class Content(Piece):
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)

View file

@ -20,7 +20,6 @@ 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
from catalog.models import *
from mastodon.api import boost_toot
from takahe.utils import Takahe
from users.models import APIdentity
@ -137,7 +136,8 @@ class Mark:
or visibility != self.visibility
)
if shelf_type is None or visibility != self.visibility:
Takahe.delete_mark(self)
if self.shelfmember:
Takahe.delete_posts(self.shelfmember.all_post_ids)
if created_time and created_time >= timezone.now():
created_time = None
post_as_new = shelf_type != self.shelf_type or visibility != self.visibility

View file

@ -59,12 +59,12 @@ class Rating(Content):
"grade": value,
"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)
p.link_post_id(post_id)
return p
@staticmethod

View file

@ -92,12 +92,12 @@ class ShelfMember(ListMember):
"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)
p.link_post_id(post_id)
return p
@cached_property
@ -277,12 +277,11 @@ class ShelfManager:
@classmethod
def get_action_label(
cls, shelf_type: ShelfType, item_category: ItemCategory
cls, shelf_type: ShelfType | str, 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)
st = str(shelf_type)
sts = [n[2] for n in ShelfTypeNames if n[0] == item_category and n[1] == st]
return sts[0] if sts else st
@classmethod
def get_label(cls, shelf_type: ShelfType, item_category: ItemCategory):

View file

@ -0,0 +1,9 @@
<span>
<a hx-get="{% url 'journal:piece_replies' piece.uuid %}"
hx-swap="outerHTML"
hx-trigger="click once"
hx-target="#replies_{{ post.pk }}">
<i class="fa-solid fa-reply"></i>
{% if post.stats.replies %}<span>{{ post.stats.replies }}</span>{% endif %}
</a>
</span>

View file

@ -23,9 +23,9 @@ def wish_item_action(context, item):
def like_piece_action(context, piece):
user = context["request"].user
action = {}
if user and user.is_authenticated and piece and piece.post_id:
if user and user.is_authenticated and piece and piece.latest_post:
action = {
"taken": Takahe.post_liked_by(piece.post_id, user),
"taken": Takahe.post_liked_by(piece.latest_post.pk, user),
"url": reverse("journal:like", args=[piece.uuid]),
}
return action

View file

@ -51,8 +51,9 @@ def like(request: AuthedHttpRequest, piece_uuid):
piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid))
if not piece:
raise Http404()
if piece.post_id:
Takahe.like_post(piece.post_id, request.user.identity.pk)
post = piece.latest_post
if post:
Takahe.like_post(post.pk, request.user.identity.pk)
if request.GET.get("back"):
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
elif request.GET.get("stats"):
@ -76,8 +77,9 @@ def unlike(request: AuthedHttpRequest, piece_uuid):
piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid))
if not piece:
raise Http404()
if piece.post_id:
Takahe.unlike_post(piece.post_id, request.user.identity.pk)
post = piece.latest_post
if post:
Takahe.unlike_post(post.pk, request.user.identity.pk)
if request.GET.get("back"):
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
elif request.GET.get("stats"):

View file

@ -4,7 +4,6 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from catalog.models import *
from common.utils import (
AuthedHttpRequest,
PageLinksGenerator,
@ -23,12 +22,14 @@ def piece_replies(request: AuthedHttpRequest, piece_uuid: str):
if not piece.is_visible_to(request.user):
raise PermissionDenied()
replies = piece.get_replies(request.user.identity)
return render(request, "replies.html", {"post": piece.post, "replies": replies})
return render(
request, "replies.html", {"post": piece.latest_post, "replies": replies}
)
@login_required
def post_replies(request: AuthedHttpRequest, post_id: int):
replies = Takahe.get_post_replies(post_id, request.user.identity.pk)
replies = Takahe.get_replies_for_posts([post_id], request.user.identity.pk)
return render(
request, "replies.html", {"post": Takahe.get_post(post_id), "replies": replies}
)
@ -41,7 +42,7 @@ def post_reply(request: AuthedHttpRequest, post_id: int):
if request.method != "POST" or not content:
raise BadRequest()
Takahe.reply_post(post_id, request.user.identity.pk, content, visibility)
replies = Takahe.get_post_replies(post_id, request.user.identity.pk)
replies = Takahe.get_replies_for_posts([post_id], request.user.identity.pk)
return render(
request, "replies.html", {"post": Takahe.get_post(post_id), "replies": replies}
)

View file

@ -97,7 +97,7 @@ def post_updated(pk, obj):
def post_deleted(pk, obj):
Piece.objects.filter(post_id=pk, local=False).delete()
Piece.objects.filter(posts__id=pk, local=False).delete()
def user_follow_updated(source_identity_pk, target_identity_pk):

View file

@ -377,18 +377,21 @@ class Takahe:
return post
@staticmethod
def get_post(post_pk: int) -> str | None:
def get_post(post_pk: int) -> Post | None:
return Post.objects.filter(pk=post_pk).first()
@staticmethod
def get_posts(post_pks: list[int]):
return Post.objects.filter(pk__in=post_pks)
@staticmethod
def get_post_url(post_pk: int) -> str | None:
post = Post.objects.filter(pk=post_pk).first() if post_pk else None
return post.object_uri if post else None
@staticmethod
def delete_mark(mark):
if mark.shelfmember and mark.shelfmember.post_id:
Post.objects.filter(pk=mark.shelfmember.post_id).update(state="deleted")
def delete_posts(post_pks):
Post.objects.filter(pk__in=post_pks).update(state="deleted")
@staticmethod
def post_mark(mark, share_as_new_post: bool) -> Post | None:
@ -428,26 +431,21 @@ class Takahe:
v = Takahe.Visibilities.public
else:
v = Takahe.Visibilities.unlisted
existing_post = None if share_as_new_post else mark.shelfmember.latest_post
post = Takahe.post(
mark.owner.pk,
pre_conetent,
content,
v,
data,
None if share_as_new_post else mark.shelfmember.post_id,
existing_post.pk if existing_post else None,
mark.shelfmember.created_time,
)
if not post:
return
if post.pk != mark.shelfmember.post_id:
mark.shelfmember.post_id = post.pk
mark.shelfmember.save(update_fields=["post_id"])
if mark.comment and post.pk != mark.comment.post_id:
mark.comment.post_id = post.pk
mark.comment.save(update_fields=["post_id"])
if mark.rating and post.pk != mark.rating.post_id:
mark.rating.post_id = post.pk
mark.rating.save(update_fields=["post_id"])
for piece in [mark.shelfmember, mark.comment, mark.rating]:
if piece:
piece.link_post(post)
return post
@staticmethod
@ -522,11 +520,11 @@ class Takahe:
return post.stats or {}
@staticmethod
def get_post_replies(post_pk: int | None, identity_pk: int | None):
if not post_pk:
return Post.objects.none()
node = Post.objects.filter(pk=post_pk).first()
if not node:
def get_replies_for_posts(post_pks: list[int], identity_pk: int | None):
post_uris = Post.objects.filter(pk__in=post_pks).values_list(
"object_uri", flat=True
)
if not post_uris.exists():
return Post.objects.none()
identity = (
Identity.objects.filter(pk=identity_pk).first() if identity_pk else None
@ -542,7 +540,7 @@ class Takahe:
"author",
"author__domain",
)
.filter(in_reply_to=node.object_uri)
.filter(in_reply_to__in=post_uris)
.order_by("published")
)
if identity: