enable multiple posts associate with one piece
This commit is contained in:
parent
a761eaca19
commit
4cd1f6b10d
15 changed files with 188 additions and 64 deletions
|
@ -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
|
||||
|
|
|
@ -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 }}"
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
25
journal/migrations/0017_alter_piece_options_and_more.py
Normal file
25
journal/migrations/0017_alter_piece_options_and_more.py
Normal 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",
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
9
journal/templates/action_reply_piece.html
Normal file
9
journal/templates/action_reply_piece.html
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue