diff --git a/common/api.py b/common/api.py
index d0a29c3c..c9e369bd 100644
--- a/common/api.py
+++ b/common/api.py
@@ -95,3 +95,6 @@ api = NinjaAPI(
version="1.0.0",
description=f"{settings.SITE_INFO['site_name']} API
Learn more",
)
+
+NOT_FOUND = 404, {"message": "Note not found"}
+OK = 200, {"message": "OK"}
diff --git a/common/templates/console.html b/common/templates/console.html
index 15a31566..26221ebc 100644
--- a/common/templates/console.html
+++ b/common/templates/console.html
@@ -3,7 +3,7 @@
+ href="{{ cdn_url }}/npm/swagger-ui-dist@5.18.2/swagger-ui.min.css">
{{ api.title }} Developer Console
{% include "common_libs.html" %}
diff --git a/journal/api.py b/journal/api.py
deleted file mode 100644
index 1b5ad088..00000000
--- a/journal/api.py
+++ /dev/null
@@ -1,604 +0,0 @@
-from datetime import datetime
-from typing import List
-
-from django.http import HttpResponse
-from ninja import Field, Schema
-from ninja.pagination import paginate
-
-from catalog.common.models import *
-from common.api import *
-
-from .models import (
- Collection,
- Mark,
- Review,
- ShelfType,
- Tag,
- q_item_in_category,
-)
-
-# Mark
-
-
-class MarkSchema(Schema):
- shelf_type: ShelfType
- visibility: int = Field(ge=0, le=2)
- item: ItemSchema
- created_time: datetime
- comment_text: str | None
- rating_grade: int | None = Field(ge=1, le=10)
- tags: list[str]
-
-
-class MarkInSchema(Schema):
- shelf_type: ShelfType
- visibility: int = Field(ge=0, le=2)
- comment_text: str = ""
- rating_grade: int = Field(0, ge=0, le=10)
- tags: list[str] = []
- created_time: datetime | None = None
- post_to_fediverse: bool = False
-
-
-@api.get(
- "/me/shelf/{type}",
- response={200: List[MarkSchema], 401: Result, 403: Result},
- tags=["mark"],
-)
-@paginate(PageNumberPagination)
-def list_marks_on_shelf(
- request, type: ShelfType, category: AvailableItemCategory | None = None
-):
- """
- Get holding marks on current user's shelf
-
- Shelf's `type` should be one of `wishlist` / `progress` / `complete`;
- `category` is optional, marks for all categories will be returned if not specified.
- """
- queryset = request.user.shelf_manager.get_latest_members(
- type, category
- ).prefetch_related("item")
- return queryset
-
-
-@api.get(
- "/me/shelf/item/{item_uuid}",
- response={200: MarkSchema, 302: Result, 401: Result, 403: Result, 404: Result},
- tags=["mark"],
-)
-def get_mark_by_item(request, item_uuid: str, response: HttpResponse):
- """
- Get holding mark on current user's shelf by item uuid
- """
- item = Item.get_by_url(item_uuid)
- if not item or item.is_deleted:
- return 404, {"message": "Item not found"}
- if item.merged_to_item:
- response["Location"] = f"/api/me/shelf/item/{item.merged_to_item.uuid}"
- return 302, {"message": "Item merged", "url": item.merged_to_item.api_url}
- shelfmember = request.user.shelf_manager.locate_item(item)
- if not shelfmember:
- return 404, {"message": "Mark not found"}
- return shelfmember
-
-
-@api.post(
- "/me/shelf/item/{item_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["mark"],
-)
-def mark_item(request, item_uuid: str, mark: MarkInSchema):
- """
- Create or update a holding mark about an item for current user.
-
- `shelf_type` and `visibility` are required; `created_time` is optional, default to now.
- if the item is already marked, this will update the mark.
-
- updating mark without `rating_grade`, `comment_text` or `tags` field will clear them.
- """
- item = Item.get_by_url(item_uuid)
- if not item or item.is_deleted or item.merged_to_item:
- return 404, {"message": "Item not found"}
- if mark.created_time and mark.created_time >= timezone.now():
- mark.created_time = None
- m = Mark(request.user.identity, item)
- m.update(
- mark.shelf_type,
- mark.comment_text,
- mark.rating_grade,
- mark.tags,
- mark.visibility,
- created_time=mark.created_time,
- share_to_mastodon=mark.post_to_fediverse,
- )
- return 200, {"message": "OK"}
-
-
-@api.delete(
- "/me/shelf/item/{item_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["mark"],
-)
-def delete_mark(request, item_uuid: str):
- """
- Remove a holding mark about an item for current user, unlike the web behavior, this does not clean up tags.
- """
- item = Item.get_by_url(item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- m = Mark(request.user.identity, item)
- m.delete(keep_tags=True)
- return 200, {"message": "OK"}
-
-
-# Review
-
-
-class ReviewSchema(Schema):
- url: str
- visibility: int = Field(ge=0, le=2)
- item: ItemSchema
- created_time: datetime
- title: str
- body: str
- html_content: str
-
-
-class ReviewInSchema(Schema):
- visibility: int = Field(ge=0, le=2)
- created_time: datetime | None = None
- title: str
- body: str
- post_to_fediverse: bool = False
-
-
-@api.get(
- "/me/review/",
- response={200: List[ReviewSchema], 401: Result, 403: Result},
- tags=["review"],
-)
-@paginate(PageNumberPagination)
-def list_reviews(request, category: AvailableItemCategory | None = None):
- """
- Get reviews by current user
-
- `category` is optional, reviews for all categories will be returned if not specified.
- """
- queryset = Review.objects.filter(owner=request.user.identity)
- if category:
- queryset = queryset.filter(q_item_in_category(category)) # type: ignore[arg-type]
- return queryset.prefetch_related("item")
-
-
-@api.get(
- "/me/review/item/{item_uuid}",
- response={200: ReviewSchema, 401: Result, 403: Result, 404: Result},
- tags=["review"],
-)
-def get_review_by_item(request, item_uuid: str):
- """
- Get review on current user's shelf by item uuid
- """
- item = Item.get_by_url(item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- review = Review.objects.filter(owner=request.user.identity, item=item).first()
- if not review:
- return 404, {"message": "Review not found"}
- return review
-
-
-@api.post(
- "/me/review/item/{item_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["review"],
-)
-def review_item(request, item_uuid: str, review: ReviewInSchema):
- """
- Create or update a review about an item for current user.
-
- `title`, `body` (markdown formatted) and`visibility` are required;
- `created_time` is optional, default to now.
- if the item is already reviewed, this will update the review.
- """
- item = Item.get_by_url(item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- if review.created_time and review.created_time >= timezone.now():
- review.created_time = None
- Review.update_item_review(
- item,
- request.user.identity,
- review.title,
- review.body,
- review.visibility,
- created_time=review.created_time,
- share_to_mastodon=review.post_to_fediverse,
- )
- return 200, {"message": "OK"}
-
-
-@api.delete(
- "/me/review/item/{item_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["review"],
-)
-def delete_review(request, item_uuid: str):
- """
- Remove a review about an item for current user.
- """
- item = Item.get_by_url(item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- Review.update_item_review(item, request.user.identity, None, None)
- return 200, {"message": "OK"}
-
-
-# Collection
-
-
-class CollectionSchema(Schema):
- uuid: str
- url: str
- visibility: int = Field(ge=0, le=2)
- created_time: datetime
- title: str
- brief: str
- cover: str
- html_content: str
-
-
-class CollectionInSchema(Schema):
- title: str
- brief: str
- visibility: int = Field(ge=0, le=2)
-
-
-class CollectionItemSchema(Schema):
- item: ItemSchema
- note: str
-
-
-class CollectionItemInSchema(Schema):
- item_uuid: str
- note: str
-
-
-@api.get(
- "/me/collection/",
- response={200: List[CollectionSchema], 401: Result, 403: Result},
- tags=["collection"],
-)
-@paginate(PageNumberPagination)
-def list_collections(request):
- """
- Get collections created by current user
- """
- queryset = Collection.objects.filter(owner=request.user.identity)
- return queryset
-
-
-@api.get(
- "/me/collection/{collection_uuid}",
- response={200: CollectionSchema, 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-def get_collection(request, collection_uuid: str):
- """
- Get collections by its uuid
- """
- c = Collection.get_by_url(collection_uuid)
- if not c:
- return 404, {"message": "Collection not found"}
- if c.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- return c
-
-
-@api.post(
- "/me/collection/",
- response={200: CollectionSchema, 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-def create_collection(request, c_in: CollectionInSchema):
- """
- Create collection.
-
- `title`, `brief` (markdown formatted) and `visibility` are required;
- """
- c = Collection.objects.create(
- owner=request.user.identity,
- title=c_in.title,
- brief=c_in.brief,
- visibility=c_in.visibility,
- )
- return c
-
-
-@api.put(
- "/me/collection/{collection_uuid}",
- response={200: CollectionSchema, 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-def update_collection(request, collection_uuid: str, c_in: CollectionInSchema):
- """
- Update collection.
- """
- c = Collection.get_by_url(collection_uuid)
- if not c:
- return 404, {"message": "Collection not found"}
- if c.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- c.title = c_in.title
- c.brief = c_in.brief
- c.visibility = c_in.visibility
- c.save()
- return c
-
-
-@api.delete(
- "/me/collection/{collection_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-def delete_collection(request, collection_uuid: str):
- """
- Remove a collection.
- """
- c = Collection.get_by_url(collection_uuid)
- if not c:
- return 404, {"message": "Collection not found"}
- if c.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- c.delete()
- return 200, {"message": "OK"}
-
-
-@api.get(
- "/me/collection/{collection_uuid}/item/",
- response={200: List[CollectionItemSchema], 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-@paginate(PageNumberPagination)
-def collection_list_items(request, collection_uuid: str):
- """
- Get items in a collection collections
- """
- c = Collection.get_by_url(collection_uuid)
- if not c:
- return 404, {"message": "Collection not found"}
- if c.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- return c.ordered_members
-
-
-@api.post(
- "/me/collection/{collection_uuid}/item/",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-def collection_add_item(
- request, collection_uuid: str, collection_item: CollectionItemInSchema
-):
- """
- Add an item to collection
- """
- c = Collection.get_by_url(collection_uuid)
- if not c:
- return 404, {"message": "Collection not found"}
- if c.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- if not collection_item.item_uuid:
- return 404, {"message": "Item not found"}
- item = Item.get_by_url(collection_item.item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- c.append_item(item, note=collection_item.note)
- return 200, {"message": "OK"}
-
-
-@api.delete(
- "/me/collection/{collection_uuid}/item/{item_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["collection"],
-)
-def collection_delete_item(request, collection_uuid: str, item_uuid: str):
- """
- Remove an item from collection
- """
- c = Collection.get_by_url(collection_uuid)
- if not c:
- return 404, {"message": "Collection not found"}
- if c.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- item = Item.get_by_url(item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- c.remove_item(item)
- return 200, {"message": "OK"}
-
-
-class TagSchema(Schema):
- uuid: str
- title: str
- visibility: int = Field(ge=0, le=2)
-
-
-class TagInSchema(Schema):
- title: str
- visibility: int = Field(ge=0, le=2)
-
-
-class TagItemSchema(Schema):
- item: ItemSchema
-
-
-class TagItemInSchema(Schema):
- item_uuid: str
-
-
-@api.get(
- "/me/tag/",
- response={200: List[TagSchema], 401: Result, 403: Result},
- tags=["tag"],
-)
-@paginate(PageNumberPagination)
-def list_tags(request, title: str | None = None):
- """
- Get tags created by current user
-
- `title` is optional, all tags will be returned if not specified.
- """
- queryset = Tag.objects.filter(owner=request.user.identity)
- if title:
- queryset = queryset.filter(title=Tag.cleanup_title(title))
- return queryset
-
-
-@api.get(
- "/me/tag/{tag_uuid}",
- response={200: TagSchema, 401: Result, 403: Result, 404: Result},
- tags=["tag"],
-)
-def get_tag(request, tag_uuid: str):
- """
- Get tags by its uuid
- """
- tag = Tag.get_by_url(tag_uuid)
- if not tag:
- return 404, {"message": "Tag not found"}
- if tag.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- return tag
-
-
-@api.post(
- "/me/tag/",
- response={200: TagSchema, 401: Result, 403: Result, 404: Result},
- tags=["tag"],
-)
-def create_tag(request, t_in: TagInSchema):
- """
- Create tag.
-
- `title` is required, `visibility` can only be 0 or 2; if tag with same title exists, existing tag will be returned.
- """
- title = Tag.cleanup_title(t_in.title)
- visibility = 2 if t_in.visibility else 0
- tag, created = Tag.objects.get_or_create(
- owner=request.user.identity,
- title=title,
- defaults={"visibility": visibility},
- )
- if not created:
- tag.visibility = visibility
- tag.save()
- return tag
-
-
-@api.put(
- "/me/tag/{tag_uuid}",
- response={200: TagSchema, 401: Result, 403: Result, 404: Result, 409: Result},
- tags=["tag"],
-)
-def update_tag(request, tag_uuid: str, t_in: TagInSchema):
- """
- Update tag.
-
- rename tag with an existing title will return HTTP 409 error
- """
- tag = Tag.get_by_url(tag_uuid)
- if not tag:
- return 404, {"message": "Tag not found"}
- if tag.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- title = Tag.cleanup_title(tag.title)
- visibility = 2 if t_in.visibility else 0
- if title != tag.title:
- try:
- tag.title = title
- tag.visibility = visibility
- tag.save()
- except Exception:
- return 409, {"message": "Tag with same title exists"}
- return tag
-
-
-@api.delete(
- "/me/tag/{tag_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["tag"],
-)
-def delete_tag(request, tag_uuid: str):
- """
- Remove a tag.
- """
- tag = Tag.get_by_url(tag_uuid)
- if not tag:
- return 404, {"message": "Tag not found"}
- if tag.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- tag.delete()
- return 200, {"message": "OK"}
-
-
-@api.get(
- "/me/tag/{tag_uuid}/item/",
- response={200: List[TagItemSchema], 401: Result, 403: Result, 404: Result},
- tags=["tag"],
-)
-@paginate(PageNumberPagination)
-def tag_list_items(request, tag_uuid: str):
- """
- Get items in a tag tags
- """
- tag = Tag.get_by_url(tag_uuid)
- if not tag:
- return 404, {"message": "Tag not found"}
- if tag.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- return tag.members.all()
-
-
-@api.post(
- "/me/tag/{tag_uuid}/item/",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["tag"],
-)
-def tag_add_item(request, tag_uuid: str, tag_item: TagItemInSchema):
- """
- Add an item to tag
- """
- tag = Tag.get_by_url(tag_uuid)
- if not tag:
- return 404, {"message": "Tag not found"}
- if tag.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- if not tag_item.item_uuid:
- return 404, {"message": "Item not found"}
- item = Item.get_by_url(tag_item.item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- tag.append_item(item)
- return 200, {"message": "OK"}
-
-
-@api.delete(
- "/me/tag/{tag_uuid}/item/{item_uuid}",
- response={200: Result, 401: Result, 403: Result, 404: Result},
- tags=["tag"],
-)
-def tag_delete_item(request, tag_uuid: str, item_uuid: str):
- """
- Remove an item from tag
- """
- tag = Tag.get_by_url(tag_uuid)
- if not tag:
- return 404, {"message": "Tag not found"}
- if tag.owner != request.user.identity:
- return 403, {"message": "Not owner"}
- item = Item.get_by_url(item_uuid)
- if not item:
- return 404, {"message": "Item not found"}
- tag.remove_item(item)
- return 200, {"message": "OK"}
diff --git a/journal/apis/__init__.py b/journal/apis/__init__.py
new file mode 100644
index 00000000..0cf9e977
--- /dev/null
+++ b/journal/apis/__init__.py
@@ -0,0 +1,5 @@
+from .collection import * # noqa
+from .note import * # noqa
+from .review import * # noqa
+from .shelf import * # noqa
+from .tag import * # noqa
diff --git a/journal/apis/collection.py b/journal/apis/collection.py
new file mode 100644
index 00000000..37b33678
--- /dev/null
+++ b/journal/apis/collection.py
@@ -0,0 +1,192 @@
+from datetime import datetime
+from typing import List
+
+from ninja import Field, Schema
+from ninja.pagination import paginate
+
+from catalog.common.models import Item, ItemSchema
+from common.api import PageNumberPagination, Result, api
+
+from ..models import Collection
+
+
+class CollectionSchema(Schema):
+ uuid: str
+ url: str
+
+ visibility: int = Field(ge=0, le=2)
+ created_time: datetime
+ title: str
+ brief: str
+ cover: str
+ html_content: str
+
+
+class CollectionInSchema(Schema):
+ title: str
+ brief: str
+ visibility: int = Field(ge=0, le=2)
+
+
+class CollectionItemSchema(Schema):
+ item: ItemSchema
+ note: str
+
+
+class CollectionItemInSchema(Schema):
+ item_uuid: str
+ note: str
+
+
+@api.get(
+ "/me/collection/",
+ response={200: List[CollectionSchema], 401: Result, 403: Result},
+ tags=["collection"],
+)
+@paginate(PageNumberPagination)
+def list_collections(request):
+ """
+ Get collections created by current user
+ """
+ queryset = Collection.objects.filter(owner=request.user.identity)
+ return queryset
+
+
+@api.get(
+ "/me/collection/{collection_uuid}",
+ response={200: CollectionSchema, 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+def get_collection(request, collection_uuid: str):
+ """
+ Get collections by its uuid
+ """
+ c = Collection.get_by_url(collection_uuid)
+ if not c:
+ return 404, {"message": "Collection not found"}
+ if c.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ return c
+
+
+@api.post(
+ "/me/collection/",
+ response={200: CollectionSchema, 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+def create_collection(request, c_in: CollectionInSchema):
+ """
+ Create collection.
+
+ `title`, `brief` (markdown formatted) and `visibility` are required;
+ """
+ c = Collection.objects.create(
+ owner=request.user.identity,
+ title=c_in.title,
+ brief=c_in.brief,
+ visibility=c_in.visibility,
+ )
+ return c
+
+
+@api.put(
+ "/me/collection/{collection_uuid}",
+ response={200: CollectionSchema, 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+def update_collection(request, collection_uuid: str, c_in: CollectionInSchema):
+ """
+ Update collection.
+ """
+ c = Collection.get_by_url(collection_uuid)
+ if not c:
+ return 404, {"message": "Collection not found"}
+ if c.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ c.title = c_in.title
+ c.brief = c_in.brief
+ c.visibility = c_in.visibility
+ c.save()
+ return c
+
+
+@api.delete(
+ "/me/collection/{collection_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+def delete_collection(request, collection_uuid: str):
+ """
+ Remove a collection.
+ """
+ c = Collection.get_by_url(collection_uuid)
+ if not c:
+ return 404, {"message": "Collection not found"}
+ if c.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ c.delete()
+ return 200, {"message": "OK"}
+
+
+@api.get(
+ "/me/collection/{collection_uuid}/item/",
+ response={200: List[CollectionItemSchema], 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+@paginate(PageNumberPagination)
+def collection_list_items(request, collection_uuid: str):
+ """
+ Get items in a collection collections
+ """
+ c = Collection.get_by_url(collection_uuid)
+ if not c:
+ return 404, {"message": "Collection not found"}
+ if c.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ return c.ordered_members
+
+
+@api.post(
+ "/me/collection/{collection_uuid}/item/",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+def collection_add_item(
+ request, collection_uuid: str, collection_item: CollectionItemInSchema
+):
+ """
+ Add an item to collection
+ """
+ c = Collection.get_by_url(collection_uuid)
+ if not c:
+ return 404, {"message": "Collection not found"}
+ if c.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ if not collection_item.item_uuid:
+ return 404, {"message": "Item not found"}
+ item = Item.get_by_url(collection_item.item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ c.append_item(item, note=collection_item.note)
+ return 200, {"message": "OK"}
+
+
+@api.delete(
+ "/me/collection/{collection_uuid}/item/{item_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["collection"],
+)
+def collection_delete_item(request, collection_uuid: str, item_uuid: str):
+ """
+ Remove an item from collection
+ """
+ c = Collection.get_by_url(collection_uuid)
+ if not c:
+ return 404, {"message": "Collection not found"}
+ if c.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ c.remove_item(item)
+ return 200, {"message": "OK"}
diff --git a/journal/apis/note.py b/journal/apis/note.py
new file mode 100644
index 00000000..7efa1b84
--- /dev/null
+++ b/journal/apis/note.py
@@ -0,0 +1,112 @@
+from datetime import datetime
+from typing import List
+
+from ninja import Field, Schema
+from ninja.pagination import paginate
+
+from catalog.common.models import Item, ItemSchema
+from common.api import NOT_FOUND, OK, PageNumberPagination, Result, api
+
+from ..models import Note
+
+
+class NoteSchema(Schema):
+ uuid: str
+ item: ItemSchema
+ title: str
+ content: str
+ sensitive: bool = False
+ progress_type: Note.ProgressType | None = None
+ progress_value: str | None = None
+ visibility: int = Field(ge=0, le=2)
+ created_time: datetime
+
+
+class NoteInSchema(Schema):
+ title: str
+ content: str
+ sensitive: bool = False
+ progress_type: Note.ProgressType | None = None
+ progress_value: str | None = None
+ visibility: int = Field(ge=0, le=2)
+ post_to_fediverse: bool = False
+
+
+@api.get(
+ "/me/note/item/{item_uuid}/",
+ response={200: List[NoteSchema], 401: Result, 403: Result},
+ tags=["note"],
+)
+@paginate(PageNumberPagination)
+def list_notes_for_item(request, item_uuid):
+ """
+ List notes by current user for an item
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ queryset = Note.objects.filter(owner=request.user.identity, item=item)
+ return queryset.prefetch_related("item")
+
+
+@api.post(
+ "/me/note/item/{item_uuid}/",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["note"],
+)
+def add_note_for_item(request, item_uuid: str, n_in: NoteInSchema):
+ """
+ Add a note for an item
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ note = Note()
+ note.title = n_in.title
+ note.content = n_in.content
+ note.sensitive = n_in.sensitive
+ note.progress_type = n_in.progress_type
+ note.progress_value = n_in.progress_value
+ note.visibility = n_in.visibility
+ note.crosspost_when_save = n_in.post_to_fediverse
+ note.save()
+ return 200, {"message": "OK"}
+
+
+@api.put(
+ "/me/note/{note_uuid}",
+ response={200: NoteSchema, 401: Result, 403: Result, 404: Result},
+ tags=["note"],
+)
+def update_note(request, tag_uuid: str, n_in: NoteInSchema):
+ """
+ Update a note.
+ """
+ note = Note.get_by_url_and_owner(tag_uuid, request.user.identity.pk)
+ if not note:
+ return NOT_FOUND
+ note.title = n_in.title
+ note.content = n_in.content
+ note.sensitive = n_in.sensitive
+ note.progress_type = n_in.progress_type
+ note.progress_value = n_in.progress_value
+ note.visibility = n_in.visibility
+ note.crosspost_when_save = n_in.post_to_fediverse
+ note.save()
+ return note
+
+
+@api.delete(
+ "/me/note/{note_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["note"],
+)
+def delete_note(request, tag_uuid: str):
+ """
+ Delete a note.
+ """
+ note = Note.get_by_url_and_owner(tag_uuid, request.user.identity.pk)
+ if not note:
+ return NOT_FOUND
+ note.delete()
+ return OK
diff --git a/journal/apis/review.py b/journal/apis/review.py
new file mode 100644
index 00000000..93d6a725
--- /dev/null
+++ b/journal/apis/review.py
@@ -0,0 +1,115 @@
+from datetime import datetime
+from typing import List
+
+from django.utils import timezone
+from ninja import Field, Schema
+from ninja.pagination import paginate
+
+from catalog.common.models import AvailableItemCategory, Item, ItemSchema
+from common.api import PageNumberPagination, Result, api
+
+from ..models import (
+ Review,
+ q_item_in_category,
+)
+
+
+class ReviewSchema(Schema):
+ url: str
+
+ visibility: int = Field(ge=0, le=2)
+ item: ItemSchema
+ created_time: datetime
+ title: str
+ body: str
+ html_content: str
+
+
+class ReviewInSchema(Schema):
+ visibility: int = Field(ge=0, le=2)
+ created_time: datetime | None = None
+ title: str
+ body: str
+ post_to_fediverse: bool = False
+
+
+@api.get(
+ "/me/review/",
+ response={200: List[ReviewSchema], 401: Result, 403: Result},
+ tags=["review"],
+)
+@paginate(PageNumberPagination)
+def list_reviews(request, category: AvailableItemCategory | None = None):
+ """
+ Get reviews by current user
+
+ `category` is optional, reviews for all categories will be returned if not specified.
+ """
+ queryset = Review.objects.filter(owner=request.user.identity)
+ if category:
+ queryset = queryset.filter(q_item_in_category(category)) # type: ignore[arg-type]
+ return queryset.prefetch_related("item")
+
+
+@api.get(
+ "/me/review/item/{item_uuid}",
+ response={200: ReviewSchema, 401: Result, 403: Result, 404: Result},
+ tags=["review"],
+)
+def get_review_by_item(request, item_uuid: str):
+ """
+ Get review on current user's shelf by item uuid
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ review = Review.objects.filter(owner=request.user.identity, item=item).first()
+ if not review:
+ return 404, {"message": "Review not found"}
+ return review
+
+
+@api.post(
+ "/me/review/item/{item_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["review"],
+)
+def review_item(request, item_uuid: str, review: ReviewInSchema):
+ """
+ Create or update a review about an item for current user.
+
+ `title`, `body` (markdown formatted) and`visibility` are required;
+ `created_time` is optional, default to now.
+ if the item is already reviewed, this will update the review.
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ if review.created_time and review.created_time >= timezone.now():
+ review.created_time = None
+ Review.update_item_review(
+ item,
+ request.user.identity,
+ review.title,
+ review.body,
+ review.visibility,
+ created_time=review.created_time,
+ share_to_mastodon=review.post_to_fediverse,
+ )
+ return 200, {"message": "OK"}
+
+
+@api.delete(
+ "/me/review/item/{item_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["review"],
+)
+def delete_review(request, item_uuid: str):
+ """
+ Remove a review about an item for current user.
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ Review.update_item_review(item, request.user.identity, None, None)
+ return 200, {"message": "OK"}
diff --git a/journal/apis/shelf.py b/journal/apis/shelf.py
new file mode 100644
index 00000000..66cf8555
--- /dev/null
+++ b/journal/apis/shelf.py
@@ -0,0 +1,128 @@
+from datetime import datetime
+from typing import List
+
+from django.http import HttpResponse
+from django.utils import timezone
+from ninja import Field, Schema
+from ninja.pagination import paginate
+
+from catalog.common.models import AvailableItemCategory, Item, ItemSchema
+from common.api import PageNumberPagination, Result, api
+
+from ..models import (
+ Mark,
+ ShelfType,
+)
+
+
+# Mark
+class MarkSchema(Schema):
+ shelf_type: ShelfType
+ visibility: int = Field(ge=0, le=2)
+
+ item: ItemSchema
+ created_time: datetime
+ comment_text: str | None
+ rating_grade: int | None = Field(ge=1, le=10)
+ tags: list[str]
+
+
+class MarkInSchema(Schema):
+ shelf_type: ShelfType
+ visibility: int = Field(ge=0, le=2)
+ comment_text: str = ""
+ rating_grade: int = Field(0, ge=0, le=10)
+ tags: list[str] = []
+ created_time: datetime | None = None
+ post_to_fediverse: bool = False
+
+
+@api.get(
+ "/me/shelf/{type}",
+ response={200: List[MarkSchema], 401: Result, 403: Result},
+ tags=["mark"],
+)
+@paginate(PageNumberPagination)
+def list_marks_on_shelf(
+ request, type: ShelfType, category: AvailableItemCategory | None = None
+):
+ """
+ Get holding marks on current user's shelf
+
+ Shelf's `type` should be one of `wishlist` / `progress` / `complete`;
+ `category` is optional, marks for all categories will be returned if not specified.
+ """
+ queryset = request.user.shelf_manager.get_latest_members(
+ type, category
+ ).prefetch_related("item")
+ return queryset
+
+
+@api.get(
+ "/me/shelf/item/{item_uuid}",
+ response={200: MarkSchema, 302: Result, 401: Result, 403: Result, 404: Result},
+ tags=["mark"],
+)
+def get_mark_by_item(request, item_uuid: str, response: HttpResponse):
+ """
+ Get holding mark on current user's shelf by item uuid
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item or item.is_deleted:
+ return 404, {"message": "Item not found"}
+ if item.merged_to_item:
+ response["Location"] = f"/api/me/shelf/item/{item.merged_to_item.uuid}"
+ return 302, {"message": "Item merged", "url": item.merged_to_item.api_url}
+ shelfmember = request.user.shelf_manager.locate_item(item)
+ if not shelfmember:
+ return 404, {"message": "Mark not found"}
+ return shelfmember
+
+
+@api.post(
+ "/me/shelf/item/{item_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["mark"],
+)
+def mark_item(request, item_uuid: str, mark: MarkInSchema):
+ """
+ Create or update a holding mark about an item for current user.
+
+ `shelf_type` and `visibility` are required; `created_time` is optional, default to now.
+ if the item is already marked, this will update the mark.
+
+ updating mark without `rating_grade`, `comment_text` or `tags` field will clear them.
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item or item.is_deleted or item.merged_to_item:
+ return 404, {"message": "Item not found"}
+ if mark.created_time and mark.created_time >= timezone.now():
+ mark.created_time = None
+ m = Mark(request.user.identity, item)
+ m.update(
+ mark.shelf_type,
+ mark.comment_text,
+ mark.rating_grade,
+ mark.tags,
+ mark.visibility,
+ created_time=mark.created_time,
+ share_to_mastodon=mark.post_to_fediverse,
+ )
+ return 200, {"message": "OK"}
+
+
+@api.delete(
+ "/me/shelf/item/{item_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["mark"],
+)
+def delete_mark(request, item_uuid: str):
+ """
+ Remove a holding mark about an item for current user, unlike the web behavior, this does not clean up tags.
+ """
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ m = Mark(request.user.identity, item)
+ m.delete(keep_tags=True)
+ return 200, {"message": "OK"}
diff --git a/journal/apis/tag.py b/journal/apis/tag.py
new file mode 100644
index 00000000..5b821854
--- /dev/null
+++ b/journal/apis/tag.py
@@ -0,0 +1,195 @@
+from typing import List
+
+from ninja import Field, Schema
+from ninja.pagination import paginate
+
+from catalog.common.models import Item, ItemSchema
+from common.api import PageNumberPagination, Result, api
+
+from ..models import Tag
+
+
+class TagSchema(Schema):
+ uuid: str
+ title: str
+ visibility: int = Field(ge=0, le=2)
+
+
+class TagInSchema(Schema):
+ title: str
+ visibility: int = Field(ge=0, le=2)
+
+
+class TagItemSchema(Schema):
+ item: ItemSchema
+
+
+class TagItemInSchema(Schema):
+ item_uuid: str
+
+
+@api.get(
+ "/me/tag/",
+ response={200: List[TagSchema], 401: Result, 403: Result},
+ tags=["tag"],
+)
+@paginate(PageNumberPagination)
+def list_tags(request, title: str | None = None):
+ """
+ Get tags created by current user
+
+ `title` is optional, all tags will be returned if not specified.
+ """
+ queryset = Tag.objects.filter(owner=request.user.identity)
+ if title:
+ queryset = queryset.filter(title=Tag.cleanup_title(title))
+ return queryset
+
+
+@api.get(
+ "/me/tag/{tag_uuid}",
+ response={200: TagSchema, 401: Result, 403: Result, 404: Result},
+ tags=["tag"],
+)
+def get_tag(request, tag_uuid: str):
+ """
+ Get tags by its uuid
+ """
+ tag = Tag.get_by_url(tag_uuid)
+ if not tag:
+ return 404, {"message": "Tag not found"}
+ if tag.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ return tag
+
+
+@api.post(
+ "/me/tag/",
+ response={200: TagSchema, 401: Result, 403: Result, 404: Result},
+ tags=["tag"],
+)
+def create_tag(request, t_in: TagInSchema):
+ """
+ Create tag.
+
+ `title` is required, `visibility` can only be 0 or 2; if tag with same title exists, existing tag will be returned.
+ """
+ title = Tag.cleanup_title(t_in.title)
+ visibility = 2 if t_in.visibility else 0
+ tag, created = Tag.objects.get_or_create(
+ owner=request.user.identity,
+ title=title,
+ defaults={"visibility": visibility},
+ )
+ if not created:
+ tag.visibility = visibility
+ tag.save()
+ return tag
+
+
+@api.put(
+ "/me/tag/{tag_uuid}",
+ response={200: TagSchema, 401: Result, 403: Result, 404: Result, 409: Result},
+ tags=["tag"],
+)
+def update_tag(request, tag_uuid: str, t_in: TagInSchema):
+ """
+ Update tag.
+
+ rename tag with an existing title will return HTTP 409 error
+ """
+ tag = Tag.get_by_url(tag_uuid)
+ if not tag:
+ return 404, {"message": "Tag not found"}
+ if tag.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ title = Tag.cleanup_title(tag.title)
+ visibility = 2 if t_in.visibility else 0
+ if title != tag.title:
+ try:
+ tag.title = title
+ tag.visibility = visibility
+ tag.save()
+ except Exception:
+ return 409, {"message": "Tag with same title exists"}
+ return tag
+
+
+@api.delete(
+ "/me/tag/{tag_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["tag"],
+)
+def delete_tag(request, tag_uuid: str):
+ """
+ Remove a tag.
+ """
+ tag = Tag.get_by_url(tag_uuid)
+ if not tag:
+ return 404, {"message": "Tag not found"}
+ if tag.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ tag.delete()
+ return 200, {"message": "OK"}
+
+
+@api.get(
+ "/me/tag/{tag_uuid}/item/",
+ response={200: List[TagItemSchema], 401: Result, 403: Result, 404: Result},
+ tags=["tag"],
+)
+@paginate(PageNumberPagination)
+def tag_list_items(request, tag_uuid: str):
+ """
+ Get items in a tag tags
+ """
+ tag = Tag.get_by_url(tag_uuid)
+ if not tag:
+ return 404, {"message": "Tag not found"}
+ if tag.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ return tag.members.all()
+
+
+@api.post(
+ "/me/tag/{tag_uuid}/item/",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["tag"],
+)
+def tag_add_item(request, tag_uuid: str, tag_item: TagItemInSchema):
+ """
+ Add an item to tag
+ """
+ tag = Tag.get_by_url(tag_uuid)
+ if not tag:
+ return 404, {"message": "Tag not found"}
+ if tag.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ if not tag_item.item_uuid:
+ return 404, {"message": "Item not found"}
+ item = Item.get_by_url(tag_item.item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ tag.append_item(item)
+ return 200, {"message": "OK"}
+
+
+@api.delete(
+ "/me/tag/{tag_uuid}/item/{item_uuid}",
+ response={200: Result, 401: Result, 403: Result, 404: Result},
+ tags=["tag"],
+)
+def tag_delete_item(request, tag_uuid: str, item_uuid: str):
+ """
+ Remove an item from tag
+ """
+ tag = Tag.get_by_url(tag_uuid)
+ if not tag:
+ return 404, {"message": "Tag not found"}
+ if tag.owner != request.user.identity:
+ return 403, {"message": "Not owner"}
+ item = Item.get_by_url(item_uuid)
+ if not item:
+ return 404, {"message": "Item not found"}
+ tag.remove_item(item)
+ return 200, {"message": "OK"}
diff --git a/journal/apps.py b/journal/apps.py
index 9d8477a0..55ec8a3f 100644
--- a/journal/apps.py
+++ b/journal/apps.py
@@ -9,7 +9,7 @@ class JournalConfig(AppConfig):
# load key modules in proper order, make sure class inject and signal works as expected
from catalog.models import Indexer
- from . import api # noqa
+ from . import apis # noqa
from .models import Rating, Tag
Indexer.register_list_model(Tag)
diff --git a/journal/models/collection.py b/journal/models/collection.py
index 32c61bd0..3a07ef4e 100644
--- a/journal/models/collection.py
+++ b/journal/models/collection.py
@@ -49,6 +49,8 @@ class Collection(List):
if TYPE_CHECKING:
members: models.QuerySet[CollectionMember]
url_path = "collection"
+ post_when_save = True
+ index_when_save = True
MEMBER_CLASS = CollectionMember
catalog_item = models.OneToOneField(
CatalogCollection, on_delete=models.PROTECT, related_name="journal_item"
@@ -116,8 +118,6 @@ class Collection(List):
self.catalog_item.cover = self.cover
self.catalog_item.save()
super().save(*args, **kwargs)
- self.sync_to_timeline()
- self.update_index()
def get_ap_data(self):
return {
diff --git a/journal/models/comment.py b/journal/models/comment.py
index 47d2ca07..36928986 100644
--- a/journal/models/comment.py
+++ b/journal/models/comment.py
@@ -38,7 +38,7 @@ class Comment(Content):
return d
@classmethod
- def update_by_ap_object(cls, owner, item, obj, post):
+ def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
p = cls.objects.filter(owner=owner, item=item).first()
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
return p # incoming ap object is older than what we have, no update needed
diff --git a/journal/models/common.py b/journal/models/common.py
index cd3f19ac..918da578 100644
--- a/journal/models/common.py
+++ b/journal/models/common.py
@@ -106,11 +106,32 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
posts = models.ManyToManyField(
"takahe.Post", related_name="pieces", through="PiecePost"
)
+ previous_visibility: int | None = None
+ post_when_save: bool = False
+ crosspost_when_save: bool = False
+ index_when_save: bool = False
@property
def classname(self) -> str:
return self.__class__.__name__.lower()
+ @classmethod
+ def from_db(cls, db, field_names, values):
+ instance = super().from_db(db, field_names, values)
+ instance.previous_visibility = instance.visibility
+ return instance
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+ if self.local and self.post_when_save:
+ visibility_changed = self.previous_visibility != self.visibility
+ self.previous_visibility = self.visibility
+ self.sync_to_timeline(1 if visibility_changed else 0)
+ if self.crosspost_when_save:
+ self.sync_to_social_accounts(0)
+ if self.index_when_save:
+ self.update_index()
+
def delete(self, *args, **kwargs):
if self.local:
self.delete_from_timeline()
@@ -173,6 +194,19 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
obj = None
return obj
+ @classmethod
+ def get_by_url_and_owner(cls, url_or_b62, owner_id):
+ b62 = url_or_b62.strip().split("/")[-1]
+ if len(b62) not in [21, 22]:
+ r = re.search(r"[A-Za-z0-9]{21,22}", url_or_b62)
+ if r:
+ b62 = r[0]
+ try:
+ obj = cls.objects.get(uid=uuid.UUID(int=b62_decode(b62)), owner_id=owner_id)
+ except Exception:
+ obj = None
+ return obj
+
@classmethod
def get_by_post_id(cls, post_id: int):
pp = PiecePost.objects.filter(post_id=post_id).first()
@@ -236,7 +270,12 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
@classmethod
def update_by_ap_object(
- cls, owner: APIdentity, item: Item, obj, post: "Post"
+ cls,
+ owner: APIdentity,
+ item: Item,
+ obj,
+ post: "Post",
+ crosspost: bool | None = False,
) -> Self | None:
"""
Create or update a content piece with related AP message
@@ -257,6 +296,8 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
d["edited_time"] = edited
for k, v in d.items():
setattr(p, k, v)
+ if crosspost is not None:
+ p.crosspost_when_save = crosspost
p.save(update_fields=d.keys())
else:
# no previously linked piece, create a new one and link to post
@@ -275,7 +316,10 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
else:
d["created_time"] = datetime.fromisoformat(obj["published"])
d["edited_time"] = datetime.fromisoformat(obj["updated"])
- p = cls.objects.create(**d)
+ p = cls(**d)
+ if crosspost is not None:
+ p.crosspost_when_save = crosspost
+ p.save()
p.link_post_id(post.id)
# subclass may have to add additional code to update type_data in local post
return p
diff --git a/journal/models/note.py b/journal/models/note.py
index 21dbb2e8..0aa1f750 100644
--- a/journal/models/note.py
+++ b/journal/models/note.py
@@ -27,6 +27,9 @@ _separaters = {"–", "―", "−", "—", "-"}
class Note(Content):
+ post_when_save = True
+ index_when_save = True
+
class ProgressType(models.TextChoices):
PAGE = "page", _("Page")
CHAPTER = "chapter", _("Chapter")
@@ -150,16 +153,13 @@ class Note(Content):
@override
@classmethod
- def update_by_ap_object(cls, owner, item, obj, post):
- # new_piece = cls.get_by_post_id(post.id) is None
- p = super().update_by_ap_object(owner, item, obj, post)
- if p and p.local:
- # if local piece is created from a post, update post type_data and fanout
- p.sync_to_timeline()
- if owner.user.preference.mastodon_default_repost and owner.user.mastodon:
- p.sync_to_social_accounts()
- p.update_index()
- return p
+ def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
+ crosspost = (
+ owner.local
+ and owner.user.preference.mastodon_default_repost
+ and owner.user.mastodon is not None
+ )
+ return super().update_by_ap_object(owner, item, obj, post, crosspost)
@cached_property
def shelfmember(self) -> ShelfMember | None:
diff --git a/journal/models/rating.py b/journal/models/rating.py
index eefc1ba1..b2f4d09f 100644
--- a/journal/models/rating.py
+++ b/journal/models/rating.py
@@ -39,7 +39,7 @@ class Rating(Content):
}
@classmethod
- def update_by_ap_object(cls, owner, item, obj, post):
+ def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
p = cls.objects.filter(owner=owner, item=item).first()
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
return p # incoming ap object is older than what we have, no update needed
diff --git a/journal/models/review.py b/journal/models/review.py
index bc67c432..b3b27433 100644
--- a/journal/models/review.py
+++ b/journal/models/review.py
@@ -25,6 +25,8 @@ _RE_SPOILER_TAG = re.compile(r'<(div|span)\sclass="spoiler">.*(div|span)>')
class Review(Content):
url_path = "review"
+ post_when_save = True
+ index_when_save = True
title = models.CharField(max_length=500, blank=False, null=False)
body = MarkdownxField()
@@ -64,7 +66,7 @@ class Review(Content):
}
@classmethod
- def update_by_ap_object(cls, owner, item, obj, post):
+ def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
p = cls.objects.filter(owner=owner, item=item).first()
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
return p # incoming ap object is older than what we have, no update needed
@@ -142,12 +144,10 @@ class Review(Content):
share_to_mastodon: bool = False,
):
review = Review.objects.filter(owner=owner, item=item).first()
- delete_existing_post = False
if review is not None:
if title is None:
review.delete()
return
- delete_existing_post = review.visibility != visibility
defaults = {
"title": title,
"body": body,
@@ -157,14 +157,13 @@ class Review(Content):
defaults["created_time"] = (
created_time if created_time < timezone.now() else timezone.now()
)
- review, created = cls.objects.update_or_create(
- item=item, owner=owner, defaults=defaults
- )
- update_mode = 1 if delete_existing_post else 0
- review.sync_to_timeline(update_mode)
- if share_to_mastodon:
- review.sync_to_social_accounts(update_mode)
- review.update_index()
+ if not review:
+ review = Review(item=item, owner=owner, **defaults)
+ else:
+ for name, value in defaults.items():
+ setattr(review, name, value)
+ review.crosspost_when_save = share_to_mastodon
+ review.save()
return review
def to_indexable_doc(self) -> dict[str, Any]:
diff --git a/journal/models/shelf.py b/journal/models/shelf.py
index 0a4b88e1..c41a2994 100644
--- a/journal/models/shelf.py
+++ b/journal/models/shelf.py
@@ -338,7 +338,9 @@ class ShelfMember(ListMember):
}
@classmethod
- def update_by_ap_object(cls, owner: APIdentity, item: Item, obj: dict, post):
+ def update_by_ap_object(
+ cls, owner: APIdentity, item: Item, obj: dict, post, crosspost=None
+ ):
p = cls.objects.filter(owner=owner, item=item).first()
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
return p # incoming ap object is older than what we have, no update needed
diff --git a/journal/views/note.py b/journal/views/note.py
index 1a1c3b05..74d10f95 100644
--- a/journal/views/note.py
+++ b/journal/views/note.py
@@ -98,19 +98,8 @@ def note_edit(request: AuthedHttpRequest, item_uuid: str, note_uuid: str = ""):
raise Http404(_("Content not found"))
note.delete()
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
- if note:
- orig_visibility = note.visibility
- else:
- orig_visibility = None
if not form.is_valid():
raise BadRequest(_("Invalid form data"))
+ form.instance.crosspost_when_save = form.cleaned_data["share_to_mastodon"]
note = form.save()
- delete_existing_post = (
- orig_visibility is not None and orig_visibility != note.visibility
- )
- update_mode = 1 if delete_existing_post else 0
- note.sync_to_timeline(update_mode)
- if form.cleaned_data["share_to_mastodon"]:
- note.sync_to_social_accounts(update_mode)
- note.update_index()
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
diff --git a/neodb-takahe b/neodb-takahe
index bf287254..7ff7d545 160000
--- a/neodb-takahe
+++ b/neodb-takahe
@@ -1 +1 @@
-Subproject commit bf2872547f95fd6dd8b5fea08682f3eb63ef8c99
+Subproject commit 7ff7d545cfb5e79acbecdf9ee3dbf2d06321ed28