api for notes
This commit is contained in:
parent
1d3bf81a68
commit
875dfd4711
19 changed files with 832 additions and 647 deletions
|
@ -95,3 +95,6 @@ api = NinjaAPI(
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
description=f"{settings.SITE_INFO['site_name']} API <hr/><a href='{settings.SITE_INFO['site_url']}'>Learn more</a>",
|
description=f"{settings.SITE_INFO['site_name']} API <hr/><a href='{settings.SITE_INFO['site_url']}'>Learn more</a>",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NOT_FOUND = 404, {"message": "Note not found"}
|
||||||
|
OK = 200, {"message": "OK"}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ cdn_url }}/npm/swagger-ui-dist@5.13.0/swagger-ui.min.css">
|
href="{{ cdn_url }}/npm/swagger-ui-dist@5.18.2/swagger-ui.min.css">
|
||||||
<title>{{ api.title }} Developer Console</title>
|
<title>{{ api.title }} Developer Console</title>
|
||||||
{% include "common_libs.html" %}
|
{% include "common_libs.html" %}
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
@ -28,6 +28,11 @@
|
||||||
#swagger-ui>div>div>.wrapper button.btn.execute {
|
#swagger-ui>div>div>.wrapper button.btn.execute {
|
||||||
background-color: #4990e2;
|
background-color: #4990e2;
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
:first-letter {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
604
journal/api.py
604
journal/api.py
|
@ -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"}
|
|
5
journal/apis/__init__.py
Normal file
5
journal/apis/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .collection import * # noqa
|
||||||
|
from .note import * # noqa
|
||||||
|
from .review import * # noqa
|
||||||
|
from .shelf import * # noqa
|
||||||
|
from .tag import * # noqa
|
192
journal/apis/collection.py
Normal file
192
journal/apis/collection.py
Normal file
|
@ -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"}
|
112
journal/apis/note.py
Normal file
112
journal/apis/note.py
Normal file
|
@ -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
|
115
journal/apis/review.py
Normal file
115
journal/apis/review.py
Normal file
|
@ -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"}
|
128
journal/apis/shelf.py
Normal file
128
journal/apis/shelf.py
Normal file
|
@ -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"}
|
195
journal/apis/tag.py
Normal file
195
journal/apis/tag.py
Normal file
|
@ -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"}
|
|
@ -9,7 +9,7 @@ class JournalConfig(AppConfig):
|
||||||
# load key modules in proper order, make sure class inject and signal works as expected
|
# load key modules in proper order, make sure class inject and signal works as expected
|
||||||
from catalog.models import Indexer
|
from catalog.models import Indexer
|
||||||
|
|
||||||
from . import api # noqa
|
from . import apis # noqa
|
||||||
from .models import Rating, Tag
|
from .models import Rating, Tag
|
||||||
|
|
||||||
Indexer.register_list_model(Tag)
|
Indexer.register_list_model(Tag)
|
||||||
|
|
|
@ -49,6 +49,8 @@ class Collection(List):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
members: models.QuerySet[CollectionMember]
|
members: models.QuerySet[CollectionMember]
|
||||||
url_path = "collection"
|
url_path = "collection"
|
||||||
|
post_when_save = True
|
||||||
|
index_when_save = True
|
||||||
MEMBER_CLASS = CollectionMember
|
MEMBER_CLASS = CollectionMember
|
||||||
catalog_item = models.OneToOneField(
|
catalog_item = models.OneToOneField(
|
||||||
CatalogCollection, on_delete=models.PROTECT, related_name="journal_item"
|
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.cover = self.cover
|
||||||
self.catalog_item.save()
|
self.catalog_item.save()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
self.sync_to_timeline()
|
|
||||||
self.update_index()
|
|
||||||
|
|
||||||
def get_ap_data(self):
|
def get_ap_data(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Comment(Content):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@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()
|
p = cls.objects.filter(owner=owner, item=item).first()
|
||||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||||
return p # incoming ap object is older than what we have, no update needed
|
return p # incoming ap object is older than what we have, no update needed
|
||||||
|
|
|
@ -106,11 +106,32 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
posts = models.ManyToManyField(
|
posts = models.ManyToManyField(
|
||||||
"takahe.Post", related_name="pieces", through="PiecePost"
|
"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
|
@property
|
||||||
def classname(self) -> str:
|
def classname(self) -> str:
|
||||||
return self.__class__.__name__.lower()
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
if self.local:
|
if self.local:
|
||||||
self.delete_from_timeline()
|
self.delete_from_timeline()
|
||||||
|
@ -173,6 +194,19 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
obj = None
|
obj = None
|
||||||
return obj
|
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
|
@classmethod
|
||||||
def get_by_post_id(cls, post_id: int):
|
def get_by_post_id(cls, post_id: int):
|
||||||
pp = PiecePost.objects.filter(post_id=post_id).first()
|
pp = PiecePost.objects.filter(post_id=post_id).first()
|
||||||
|
@ -236,7 +270,12 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_by_ap_object(
|
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:
|
) -> Self | None:
|
||||||
"""
|
"""
|
||||||
Create or update a content piece with related AP message
|
Create or update a content piece with related AP message
|
||||||
|
@ -257,6 +296,8 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
d["edited_time"] = edited
|
d["edited_time"] = edited
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
setattr(p, k, v)
|
setattr(p, k, v)
|
||||||
|
if crosspost is not None:
|
||||||
|
p.crosspost_when_save = crosspost
|
||||||
p.save(update_fields=d.keys())
|
p.save(update_fields=d.keys())
|
||||||
else:
|
else:
|
||||||
# no previously linked piece, create a new one and link to post
|
# no previously linked piece, create a new one and link to post
|
||||||
|
@ -275,7 +316,10 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
else:
|
else:
|
||||||
d["created_time"] = datetime.fromisoformat(obj["published"])
|
d["created_time"] = datetime.fromisoformat(obj["published"])
|
||||||
d["edited_time"] = datetime.fromisoformat(obj["updated"])
|
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)
|
p.link_post_id(post.id)
|
||||||
# subclass may have to add additional code to update type_data in local post
|
# subclass may have to add additional code to update type_data in local post
|
||||||
return p
|
return p
|
||||||
|
|
|
@ -27,6 +27,9 @@ _separaters = {"–", "―", "−", "—", "-"}
|
||||||
|
|
||||||
|
|
||||||
class Note(Content):
|
class Note(Content):
|
||||||
|
post_when_save = True
|
||||||
|
index_when_save = True
|
||||||
|
|
||||||
class ProgressType(models.TextChoices):
|
class ProgressType(models.TextChoices):
|
||||||
PAGE = "page", _("Page")
|
PAGE = "page", _("Page")
|
||||||
CHAPTER = "chapter", _("Chapter")
|
CHAPTER = "chapter", _("Chapter")
|
||||||
|
@ -150,16 +153,13 @@ class Note(Content):
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_by_ap_object(cls, owner, item, obj, post):
|
def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
|
||||||
# new_piece = cls.get_by_post_id(post.id) is None
|
crosspost = (
|
||||||
p = super().update_by_ap_object(owner, item, obj, post)
|
owner.local
|
||||||
if p and p.local:
|
and owner.user.preference.mastodon_default_repost
|
||||||
# if local piece is created from a post, update post type_data and fanout
|
and owner.user.mastodon is not None
|
||||||
p.sync_to_timeline()
|
)
|
||||||
if owner.user.preference.mastodon_default_repost and owner.user.mastodon:
|
return super().update_by_ap_object(owner, item, obj, post, crosspost)
|
||||||
p.sync_to_social_accounts()
|
|
||||||
p.update_index()
|
|
||||||
return p
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def shelfmember(self) -> ShelfMember | None:
|
def shelfmember(self) -> ShelfMember | None:
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Rating(Content):
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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()
|
p = cls.objects.filter(owner=owner, item=item).first()
|
||||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||||
return p # incoming ap object is older than what we have, no update needed
|
return p # incoming ap object is older than what we have, no update needed
|
||||||
|
|
|
@ -25,6 +25,8 @@ _RE_SPOILER_TAG = re.compile(r'<(div|span)\sclass="spoiler">.*</(div|span)>')
|
||||||
|
|
||||||
class Review(Content):
|
class Review(Content):
|
||||||
url_path = "review"
|
url_path = "review"
|
||||||
|
post_when_save = True
|
||||||
|
index_when_save = True
|
||||||
title = models.CharField(max_length=500, blank=False, null=False)
|
title = models.CharField(max_length=500, blank=False, null=False)
|
||||||
body = MarkdownxField()
|
body = MarkdownxField()
|
||||||
|
|
||||||
|
@ -64,7 +66,7 @@ class Review(Content):
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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()
|
p = cls.objects.filter(owner=owner, item=item).first()
|
||||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||||
return p # incoming ap object is older than what we have, no update needed
|
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,
|
share_to_mastodon: bool = False,
|
||||||
):
|
):
|
||||||
review = Review.objects.filter(owner=owner, item=item).first()
|
review = Review.objects.filter(owner=owner, item=item).first()
|
||||||
delete_existing_post = False
|
|
||||||
if review is not None:
|
if review is not None:
|
||||||
if title is None:
|
if title is None:
|
||||||
review.delete()
|
review.delete()
|
||||||
return
|
return
|
||||||
delete_existing_post = review.visibility != visibility
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"body": body,
|
"body": body,
|
||||||
|
@ -157,14 +157,13 @@ class Review(Content):
|
||||||
defaults["created_time"] = (
|
defaults["created_time"] = (
|
||||||
created_time if created_time < timezone.now() else timezone.now()
|
created_time if created_time < timezone.now() else timezone.now()
|
||||||
)
|
)
|
||||||
review, created = cls.objects.update_or_create(
|
if not review:
|
||||||
item=item, owner=owner, defaults=defaults
|
review = Review(item=item, owner=owner, **defaults)
|
||||||
)
|
else:
|
||||||
update_mode = 1 if delete_existing_post else 0
|
for name, value in defaults.items():
|
||||||
review.sync_to_timeline(update_mode)
|
setattr(review, name, value)
|
||||||
if share_to_mastodon:
|
review.crosspost_when_save = share_to_mastodon
|
||||||
review.sync_to_social_accounts(update_mode)
|
review.save()
|
||||||
review.update_index()
|
|
||||||
return review
|
return review
|
||||||
|
|
||||||
def to_indexable_doc(self) -> dict[str, Any]:
|
def to_indexable_doc(self) -> dict[str, Any]:
|
||||||
|
|
|
@ -338,7 +338,9 @@ class ShelfMember(ListMember):
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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()
|
p = cls.objects.filter(owner=owner, item=item).first()
|
||||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||||
return p # incoming ap object is older than what we have, no update needed
|
return p # incoming ap object is older than what we have, no update needed
|
||||||
|
|
|
@ -98,19 +98,8 @@ def note_edit(request: AuthedHttpRequest, item_uuid: str, note_uuid: str = ""):
|
||||||
raise Http404(_("Content not found"))
|
raise Http404(_("Content not found"))
|
||||||
note.delete()
|
note.delete()
|
||||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||||
if note:
|
|
||||||
orig_visibility = note.visibility
|
|
||||||
else:
|
|
||||||
orig_visibility = None
|
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
raise BadRequest(_("Invalid form data"))
|
raise BadRequest(_("Invalid form data"))
|
||||||
|
form.instance.crosspost_when_save = form.cleaned_data["share_to_mastodon"]
|
||||||
note = form.save()
|
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", "/"))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bf2872547f95fd6dd8b5fea08682f3eb63ef8c99
|
Subproject commit 7ff7d545cfb5e79acbecdf9ee3dbf2d06321ed28
|
Loading…
Add table
Reference in a new issue