add review api

This commit is contained in:
Your Name 2023-06-03 01:22:19 -04:00 committed by Henri Dickson
parent a7857d99b2
commit f92861105d
7 changed files with 158 additions and 64 deletions

View file

@ -36,7 +36,7 @@ Boofilsic/NeoDB is an open source project and free service to help users manage,
+ view home feed with friends' activities + view home feed with friends' activities
* every activity can be set as viewable to self/follower-only/public * every activity can be set as viewable to self/follower-only/public
* eligible items, e.g. podcasts and albums, are playable in feed * eligible items, e.g. podcasts and albums, are playable in feed
+ link Fediverse account and import social graph + link Fediverse account and import social graph
+ share collections and reviews to Fediverse ~~and Twitter~~ feed + share collections and reviews to Fediverse ~~and Twitter~~ feed
+ there's a plan to add ActivityPub support in the near future + there's a plan to add ActivityPub support in the near future
@ -48,8 +48,9 @@ Please see [doc/install.md](doc/install.md)
- to file a bug or request new features for NeoDB, please contact NeoDB on [Fediverse](https://mastodon.social/@neodb) or [Twitter](https://twitter.com/NeoDBsocial) - to file a bug or request new features for NeoDB, please contact NeoDB on [Fediverse](https://mastodon.social/@neodb) or [Twitter](https://twitter.com/NeoDBsocial)
## Contribution ## Contribution
- Please see [doc/development.md](doc/development.md) for some basics to start with - To build application with NeoDB API, documentation is available in [NeoDB API Developer Console](https://neodb.social/developer/)
- Join our Discord community, links available on [our Fediverse profile](https://mastodon.social/@neodb) - To help develop NeoDB, please see [doc/development.md](doc/development.md) for some basics to start with
- Join our Discord community to share your ideas/questions/creations, links available on [our Fediverse profile](https://mastodon.social/@neodb)
## Sponsor ## Sponsor
If you like this project, please consider sponsoring If you like this project, please consider sponsoring

View file

@ -45,8 +45,10 @@ class RedirectedResult(Schema):
class PageNumberPagination(NinjaPageNumberPagination): class PageNumberPagination(NinjaPageNumberPagination):
items_attribute = "data"
class Output(Schema): class Output(Schema):
items: List[Any] data: List[Any]
pages: int pages: int
count: int count: int
@ -57,6 +59,7 @@ class PageNumberPagination(NinjaPageNumberPagination):
**params: Any, **params: Any,
) -> Output: ) -> Output:
val = super().paginate_queryset(queryset, pagination, **params) val = super().paginate_queryset(queryset, pagination, **params)
val["data"] = val["items"]
val["pages"] = (val["count"] + self.page_size - 1) // self.page_size val["pages"] = (val["count"] + self.page_size - 1) // self.page_size
return val return val

View file

@ -28,13 +28,13 @@
<h5>Developer Console</h5> <h5>Developer Console</h5>
<details {% if token %}open{% endif %}> <details {% if token %}open{% endif %}>
<summary> <summary>
<b>Access Token</b> <b>Access Token Management</b>
</summary> </summary>
<form method="post" role="group"> <form method="post" role="group">
{% csrf_token %} {% csrf_token %}
<input type="text" <input type="text"
readonly readonly
value="{{ token | default:'Token will only be shown once here, previous tokens will be cleared. If you lose it, you can generate a new one.' }}"> value="{{ token | default:'Once generated, token will only be shown once here, previous tokens will be revoked.' }}">
<input type="submit" value="Generate" /> <input type="submit" value="Generate" />
</form> </form>
<p> <p>

View file

@ -32,8 +32,7 @@ class MarkInSchema(Schema):
post_to_fediverse: bool = False post_to_fediverse: bool = False
@api.get("/me/shelf", response={200: List[MarkSchema], 403: Result}) @api.get("/me/shelf/{type}", response={200: List[MarkSchema], 401: Result, 403: Result})
@protected_resource()
@paginate(PageNumberPagination) @paginate(PageNumberPagination)
def list_marks_on_shelf( def list_marks_on_shelf(
request, type: ShelfType, category: AvailableItemCategory | None = None request, type: ShelfType, category: AvailableItemCategory | None = None
@ -42,7 +41,7 @@ def list_marks_on_shelf(
Get holding marks on current user's shelf Get holding marks on current user's shelf
Shelf's `type` should be one of `wishlist` / `progress` / `complete`; Shelf's `type` should be one of `wishlist` / `progress` / `complete`;
`category` is optional, all marks will be returned if not specified. `category` is optional, marks for all categories will be returned if not specified.
""" """
queryset = request.user.shelf_manager.get_latest_members( queryset = request.user.shelf_manager.get_latest_members(
type, category type, category
@ -52,10 +51,8 @@ def list_marks_on_shelf(
@api.get( @api.get(
"/me/shelf/item/{item_uuid}", "/me/shelf/item/{item_uuid}",
response={200: MarkSchema, 403: Result, 404: Result}, response={200: MarkSchema, 401: Result, 403: Result, 404: Result},
auth=OAuthAccessTokenAuth(),
) )
# @protected_resource()
def get_mark_by_item(request, item_uuid: str): def get_mark_by_item(request, item_uuid: str):
""" """
Get holding mark on current user's shelf by item uuid Get holding mark on current user's shelf by item uuid
@ -70,9 +67,9 @@ def get_mark_by_item(request, item_uuid: str):
@api.post( @api.post(
"/me/shelf/item/{item_uuid}", response={200: Result, 403: Result, 404: Result} "/me/shelf/item/{item_uuid}",
response={200: Result, 401: Result, 403: Result, 404: Result},
) )
@protected_resource()
def mark_item(request, item_uuid: str, mark: MarkInSchema): def mark_item(request, item_uuid: str, mark: MarkInSchema):
""" """
Create or update a holding mark about an item for current user. Create or update a holding mark about an item for current user.
@ -102,9 +99,9 @@ def mark_item(request, item_uuid: str, mark: MarkInSchema):
@api.delete( @api.delete(
"/me/shelf/item/{item_uuid}", response={200: Result, 403: Result, 404: Result} "/me/shelf/item/{item_uuid}",
response={200: Result, 401: Result, 403: Result, 404: Result},
) )
@protected_resource()
def delete_mark(request, item_uuid: str): def delete_mark(request, item_uuid: str):
""" """
Remove a holding mark about an item for current user. Remove a holding mark about an item for current user.
@ -118,16 +115,106 @@ def delete_mark(request, item_uuid: str):
return 200, {"message": "OK"} return 200, {"message": "OK"}
# @api.get("/me/review/") class ReviewSchema(Schema):
# @api.get("/me/review/item/{item_uuid}") visibility: int = Field(ge=0, le=2)
# @api.post("/me/review/item/{item_uuid}") item: ItemSchema
# @api.delete("/me/review/item/{item_uuid}") created_time: datetime
title: str
body: str
html_content: str
class ReviewInSchema(Schema):
visibility: int = Field(ge=0, le=2)
created_time: datetime | None
title: str
body: str
post_to_fediverse: bool = False
@api.get("/me/review/", response={200: List[ReviewSchema], 401: Result, 403: Result})
@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)
if category:
queryset = queryset.filter(query_item_category(category))
return queryset.prefetch_related("item")
@api.get(
"/me/review/item/{item_uuid}",
response={200: ReviewSchema, 401: Result, 403: Result, 404: Result},
)
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, 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},
)
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"}
Review.review_item_by_user(
item,
request.user,
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},
)
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.review_item_by_user(item, request.user, None, None)
return 200, {"message": "OK"}
# @api.get("/me/collection/") # @api.get("/me/collection/")
# @api.post("/me/collection/")
# @api.get("/me/collection/{uuid}") # @api.get("/me/collection/{uuid}")
# @api.post("/me/collection/{uuid}") # @api.put("/me/collection/{uuid}")
# @api.delete("/me/collection/{uuid}") # @api.delete("/me/collection/{uuid}")
# @api.get("/me/collection/{uuid}/items") # @api.get("/me/collection/{uuid}/item/")
# @api.post("/me/collection/{uuid}/items") # @api.post("/me/collection/{uuid}/item/")
# @api.delete("/me/collection/{uuid}/items")
# @api.patch("/me/collection/{uuid}/items") # @api.get("/me/tag/")
# @api.post("/me/tag/")
# @api.get("/me/tag/{title}")
# @api.put("/me/tag/{title}")
# @api.delete("/me/tag/{title}")

View file

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from mastodon.api import share_review
from users.models import User from users.models import User
from catalog.common.models import Item, ItemCategory from catalog.common.models import Item, ItemCategory
from .mixins import UserOwnedObjectMixin from .mixins import UserOwnedObjectMixin
@ -286,31 +287,36 @@ class Review(Content):
def rating_grade(self): def rating_grade(self):
return Rating.get_item_rating_by_user(self.item, self.owner) return Rating.get_item_rating_by_user(self.item, self.owner)
@staticmethod @classmethod
def review_item_by_user(item, user, title, body, metadata={}, visibility=0): def review_item_by_user(
# allow multiple reviews per item per user. cls,
review = Review.objects.create( item,
owner=user, user,
item=item, title,
title=title, body,
body=body, visibility=0,
metadata=metadata, created_time=None,
visibility=visibility, share_to_mastodon=False,
) ):
"""
review = Review.objects.filter(owner=user, item=item).first()
if title is None: if title is None:
review = Review.objects.filter(owner=user, item=item).first()
if review is not None: if review is not None:
review.delete() review.delete()
review = None return None
elif review is None: defaults = {
review = Review.objects.create(owner=user, item=item, title=title, body=body, visibility=visibility) "title": title,
else: "body": body,
review.title = title "visibility": visibility,
review.body = body }
review.visibility = visibility if created_time:
review.save() defaults["created_time"] = (
""" created_time if created_time < timezone.now() else timezone.now()
)
review, created = cls.objects.update_or_create(
item=item, owner=user, defaults=defaults
)
if share_to_mastodon:
share_review(review)
return review return review

View file

@ -563,32 +563,29 @@ def review_edit(request, item_uuid, review_uuid=None):
else ReviewForm(request.POST) else ReviewForm(request.POST)
) )
if form.is_valid(): if form.is_valid():
if not review:
form.instance.owner = request.user
form.instance.edited_time = timezone.now()
mark_date = None mark_date = None
if request.POST.get("mark_anotherday"): if request.POST.get("mark_anotherday"):
mark_date = timezone.get_current_timezone().localize( mark_date = timezone.get_current_timezone().localize(
parse_datetime(request.POST.get("mark_date") + " 20:00:00") parse_datetime(request.POST.get("mark_date") + " 20:00:00")
) )
if mark_date and mark_date >= timezone.now(): body = form.instance.body
mark_date = None
if mark_date:
form.instance.created_time = mark_date
if request.POST.get("leading_space"): if request.POST.get("leading_space"):
form.instance.body = re.sub( body = re.sub(
r"^(\u2003*)( +)", r"^(\u2003*)( +)",
lambda s: "\u2003" * ((len(s[2]) + 1) // 2 + len(s[1])), lambda s: "\u2003" * ((len(s[2]) + 1) // 2 + len(s[1])),
form.instance.body, body,
flags=re.MULTILINE, flags=re.MULTILINE,
) )
form.save() review = Review.review_item_by_user(
if form.cleaned_data["share_to_mastodon"]: item,
if not share_review(form.instance): request.user,
return render_relogin(request) form.cleaned_data["title"],
return redirect( body,
reverse("journal:review_retrieve", args=[form.instance.uuid]) form.cleaned_data["visibility"],
mark_date,
form.cleaned_data["share_to_mastodon"],
) )
return redirect(reverse("journal:review_retrieve", args=[review.uuid]))
else: else:
raise BadRequest() raise BadRequest()
else: else:

View file

@ -5,6 +5,7 @@ from ninja.security import django_auth
class UserSchema(Schema): class UserSchema(Schema):
url: str
external_acct: str external_acct: str
display_name: str display_name: str
avatar: str avatar: str
@ -12,10 +13,9 @@ class UserSchema(Schema):
@api.get( @api.get(
"/me", "/me",
response={200: UserSchema, 400: Result, 403: Result}, response={200: UserSchema, 401: Result},
summary="Get current user's basic info", summary="Get current user's basic info",
) )
@protected_resource()
def me(request): def me(request):
return 200, { return 200, {
"external_acct": request.user.mastodon_username, "external_acct": request.user.mastodon_username,