add review api
This commit is contained in:
parent
a7857d99b2
commit
f92861105d
7 changed files with 158 additions and 64 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
125
journal/api.py
125
journal/api.py
|
@ -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}")
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue