diff --git a/boofilsic/urls.py b/boofilsic/urls.py index ad023bf1..ebecef4b 100644 --- a/boofilsic/urls.py +++ b/boofilsic/urls.py @@ -22,7 +22,7 @@ from common.api import api from users.views import login urlpatterns = [ - path("api/", api.urls), # type: ignore + # path("api/", api.urls), # type: ignore path("login/", login), path("markdownx/", include("markdownx.urls")), path("account/", include("users.urls")), diff --git a/journal/feeds.py b/journal/feeds.py deleted file mode 100644 index ced67294..00000000 --- a/journal/feeds.py +++ /dev/null @@ -1,70 +0,0 @@ -import mimetypes - -from django.conf import settings -from django.contrib.syndication.views import Feed - -from journal.models.renderers import render_md - -from .models import * - -MAX_ITEM_PER_TYPE = 10 - - -class ReviewFeed(Feed): - def get_object(self, request, id): - return User.get(id) - - def title(self, user): - return "%s的评论" % user.display_name if user else "无效链接" - - def link(self, user): - return user.url if user else settings.SITE_INFO["site_url"] - - def description(self, user): - return "%s的评论合集 - NeoDB" % user.display_name if user else "无效链接" - - def items(self, user): - if user is None or user.preference.no_anonymous_view: - return [] - reviews = Review.objects.filter(owner=user, visibility=0)[:MAX_ITEM_PER_TYPE] - return reviews - - def item_title(self, item: Review): - return f"{item.title} - 评论《{item.item.title}》" - - def item_description(self, item: Review): - target_html = ( - f'
\n' - ) - html = render_md(item.body) - return target_html + html - - # item_link is only needed if NewsItem has no get_absolute_url method. - def item_link(self, item: Review): - return item.absolute_url - - def item_categories(self, item): - return [item.item.category.label] - - def item_pubdate(self, item): - return item.created_time - - def item_updateddate(self, item): - return item.edited_time - - def item_enclosure_url(self, item): - return item.item.cover.url - - def item_enclosure_mime_type(self, item): - t, _ = mimetypes.guess_type(item.item.cover.url) - return t - - def item_enclosure_length(self, item): - try: - size = item.item.cover.file.size - except Exception: - size = None - return size - - def item_comments(self, item): - return item.absolute_url diff --git a/journal/migrations/0014_remove_reply_piece_ptr_remove_reply_reply_to_content_and_more.py b/journal/migrations/0014_remove_reply_piece_ptr_remove_reply_reply_to_content_and_more.py new file mode 100644 index 00000000..15006f2d --- /dev/null +++ b/journal/migrations/0014_remove_reply_piece_ptr_remove_reply_reply_to_content_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.4 on 2023-08-10 18:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("journal", "0013_remove_comment_focus_item"), + ] + + operations = [ + migrations.RemoveField( + model_name="reply", + name="piece_ptr", + ), + migrations.RemoveField( + model_name="reply", + name="reply_to_content", + ), + migrations.DeleteModel( + name="Memo", + ), + migrations.DeleteModel( + name="Reply", + ), + ] diff --git a/journal/urls.py b/journal/urls.py index fd4ecd78..a220ff15 100644 --- a/journal/urls.py +++ b/journal/urls.py @@ -1,8 +1,8 @@ from django.urls import path, re_path -from catalog.models import * +from catalog.models import item_categories -from .feeds import ReviewFeed +from .models import ShelfType from .views import * app_name = "journal" diff --git a/journal/views.py b/journal/views.py deleted file mode 100644 index fa008fa6..00000000 --- a/journal/views.py +++ /dev/null @@ -1,961 +0,0 @@ -import logging -from datetime import datetime - -from django.conf import settings -from django.contrib.auth.decorators import login_required -from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied -from django.core.paginator import Paginator -from django.db.models import Count -from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse -from django.utils import timezone -from django.utils.dateparse import parse_datetime -from django.utils.translation import gettext_lazy as _ -from user_messages import api as msg - -from catalog.models import * -from common.utils import PageLinksGenerator, get_uuid_or_404 -from journal.models.renderers import convert_leading_space_in_md -from mastodon.api import ( - get_spoiler_text, - get_status_id_by_url, - get_visibility, - post_toot, - share_collection, - share_review, -) -from users.models import User -from users.views import render_user_blocked, render_user_not_found - -from .forms import * -from .models import * - -_logger = logging.getLogger(__name__) -PAGE_SIZE = 10 - -_checkmark = "✔️".encode("utf-8") - - -@login_required -def wish(request, item_uuid): - if request.method != "POST": - raise BadRequest() - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - if not item: - raise Http404() - request.user.shelf_manager.move_item(item, ShelfType.WISHLIST) - if request.GET.get("back"): - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - return HttpResponse(_checkmark) - - -@login_required -def like(request, piece_uuid): - if request.method != "POST": - raise BadRequest() - piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid)) - if not piece: - raise Http404() - Like.user_like_piece(request.user, piece) - if request.GET.get("back"): - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - elif request.GET.get("stats"): - return render( - request, - "like_stats.html", - { - "piece": piece, - "liked": True, - "label": request.GET.get("label"), - "icon": request.GET.get("icon"), - }, - ) - return HttpResponse(_checkmark) - - -@login_required -def unlike(request, piece_uuid): - if request.method != "POST": - raise BadRequest() - piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid)) - if not piece: - raise Http404() - Like.user_unlike_piece(request.user, piece) - if request.GET.get("back"): - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - elif request.GET.get("stats"): - return render( - request, - "like_stats.html", - { - "piece": piece, - "liked": False, - "label": request.GET.get("label"), - "icon": request.GET.get("icon"), - }, - ) - return HttpResponse(_checkmark) - - -@login_required -def add_to_collection(request, item_uuid): - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - if request.method == "GET": - collections = Collection.objects.filter(owner=request.user) - return render( - request, - "add_to_collection.html", - { - "item": item, - "collections": collections, - }, - ) - else: - cid = int(request.POST.get("collection_id", default=0)) - if not cid: - cid = Collection.objects.create( - owner=request.user, title=f"{request.user.display_name}的收藏单" - ).id - collection = Collection.objects.get(owner=request.user, id=cid) - collection.append_item(item, note=request.POST.get("note")) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - - -def render_relogin(request): - return render( - request, - "common/error.html", - { - "url": reverse("users:connect") + "?domain=" + request.user.mastodon_site, - "msg": _("信息已保存,但是未能分享到联邦宇宙"), - "secondary_msg": _( - "可能是你在联邦宇宙(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦宇宙重新登录😼" - ), - }, - ) - - -@login_required -def mark(request, item_uuid): - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - mark = Mark(request.user, item) - if request.method == "GET": - tags = TagManager.get_item_tags_by_user(item, request.user) - shelf_types = [ - (n[1], n[2]) for n in iter(ShelfTypeNames) if n[0] == item.category - ] - shelf_type = request.GET.get("shelf_type", mark.shelf_type) - return render( - request, - "mark.html", - { - "item": item, - "mark": mark, - "shelf_type": shelf_type, - "tags": ",".join(tags), - "shelf_types": shelf_types, - "date_today": timezone.localdate().isoformat(), - }, - ) - elif request.method == "POST": - if request.POST.get("delete", default=False): - silence = request.POST.get("silence", False) - mark.delete(silence=silence) - if ( - silence - ): # this means the mark is deleted from mark_history, thus redirect to item page - return redirect( - reverse("catalog:retrieve", args=[item.url_path, item.uuid]) - ) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - else: - visibility = int(request.POST.get("visibility", default=0)) - rating_grade = request.POST.get("rating_grade", default=0) - rating_grade = int(rating_grade) if rating_grade else None - status = ShelfType(request.POST.get("status")) - text = request.POST.get("text") - tags = request.POST.get("tags") - tags = tags.split(",") if tags else [] - share_to_mastodon = bool( - request.POST.get("share_to_mastodon", default=False) - ) - mark_date = None - if request.POST.get("mark_anotherday"): - dt = parse_datetime(request.POST.get("mark_date", "") + " 20:00:00") - mark_date = ( - dt.replace(tzinfo=timezone.get_current_timezone()) if dt else None - ) - if mark_date and mark_date >= timezone.now(): - mark_date = None - TagManager.tag_item_by_user(item, request.user, tags, visibility) - try: - mark.update( - status, - text, - rating_grade, - visibility, - share_to_mastodon=share_to_mastodon, - created_time=mark_date, - ) - except PermissionDenied as e: - _logger.warn(f"post to mastodon error 401 {request.user}") - return render_relogin(request) - except ValueError as e: - _logger.warn(f"post to mastodon error {e} {request.user}") - err = _("内容长度超出实例限制") if str(e) == "422" else str(e) - return render( - request, - "common/error.html", - { - "msg": _("标记已保存,但是未能分享到联邦宇宙"), - "secondary_msg": err, - }, - ) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - raise BadRequest() - - -def share_comment(user, item, text, visibility, shared_link=None, position=None): - post_error = False - status_id = get_status_id_by_url(shared_link) - link = ( - item.get_absolute_url_with_position(position) if position else item.absolute_url - ) - action_label = "评论" if text else "分享" - status = f"{action_label}{ItemCategory(item.category).label}《{item.display_title}》\n{link}\n\n{text}" - spoiler, status = get_spoiler_text(status, item) - try: - response = post_toot( - user.mastodon_site, - status, - get_visibility(visibility, user), - user.mastodon_token, - False, - status_id, - spoiler, - ) - if response and response.status_code in [200, 201]: - j = response.json() - if "url" in j: - shared_link = j["url"] - except Exception as e: - if settings.DEBUG: - raise - post_error = True - return post_error, shared_link - - -@login_required -def mark_log(request, item_uuid, log_id): - """ - Delete log of one item by log id. - """ - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - mark = Mark(request.user, item) - if request.method == "POST": - if request.GET.get("delete", default=False): - if log_id: - mark.delete_log(log_id) - else: - mark.delete_all_logs() - return render(request, "_item_user_mark_history.html", {"mark": mark}) - raise BadRequest() - - -@login_required -def comment(request, item_uuid): - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - if not item.class_name in ["podcastepisode", "tvepisode"]: - raise BadRequest("不支持评论此类型的条目") - # episode = None - # if item.class_name == "tvseason": - # try: - # episode = int(request.POST.get("episode", 0)) - # except: - # episode = 0 - # if episode <= 0: - # raise BadRequest("请输入正确的集数") - comment = Comment.objects.filter(owner=request.user, item=item).first() - if request.method == "GET": - return render( - request, - f"comment.html", - { - "item": item, - "comment": comment, - }, - ) - elif request.method == "POST": - if request.POST.get("delete", default=False): - if not comment: - raise Http404() - comment.delete() - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - visibility = int(request.POST.get("visibility", default=0)) - text = request.POST.get("text") - position = None - if item.class_name == "podcastepisode": - position = request.POST.get("position") or "0:0:0" - try: - pos = datetime.strptime(position, "%H:%M:%S") - position = pos.hour * 3600 + pos.minute * 60 + pos.second - except: - if settings.DEBUG: - raise - position = None - share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False)) - shared_link = comment.metadata.get("shared_link") if comment else None - post_error = False - if share_to_mastodon and request.user.mastodon_username: - post_error, shared_link = share_comment( - request.user, item, text, visibility, shared_link, position - ) - Comment.objects.update_or_create( - owner=request.user, - item=item, - # metadata__episode=episode, - defaults={ - "text": text, - "visibility": visibility, - "metadata": { - "shared_link": shared_link, - "position": position, - }, - }, - ) - - # if comment: - # comment.visibility = visibility - # comment.text = text - # comment.metadata["position"] = position - # comment.metadata["episode"] = episode - # if shared_link: - # comment.metadata["shared_link"] = shared_link - # comment.save() - # else: - # comment = Comment.objects.create( - # owner=request.user, - # item=item, - # text=text, - # visibility=visibility, - # metadata={ - # "shared_link": shared_link, - # "position": position, - # "episode": episode, - # }, - # ) - if post_error: - return render_relogin(request) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - raise BadRequest() - - -def collection_retrieve(request, collection_uuid): - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - follower_count = collection.likes.all().count() - following = ( - Like.user_liked_piece(request.user, collection) - if request.user.is_authenticated - else False - ) - featured_since = ( - collection.featured_by_user_since(request.user) - if request.user.is_authenticated - else None - ) - available_as_featured = ( - request.user.is_authenticated - and (following or request.user == collection.owner) - and not featured_since - and collection.members.all().exists() - ) - stats = {} - if featured_since: - stats = collection.get_stats_for_user(request.user) - stats["wishlist_deg"] = ( - round(stats["wishlist"] / stats["total"] * 360) if stats["total"] else 0 - ) - stats["progress_deg"] = ( - round(stats["progress"] / stats["total"] * 360) if stats["total"] else 0 - ) - stats["complete_deg"] = ( - round(stats["complete"] / stats["total"] * 360) if stats["total"] else 0 - ) - return render( - request, - "collection.html", - { - "collection": collection, - "follower_count": follower_count, - "following": following, - "stats": stats, - "available_as_featured": available_as_featured, - "featured_since": featured_since, - }, - ) - - -@login_required -def collection_add_featured(request, collection_uuid): - if request.method != "POST": - raise BadRequest() - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - FeaturedCollection.objects.update_or_create(owner=request.user, target=collection) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - - -@login_required -def collection_remove_featured(request, collection_uuid): - if request.method != "POST": - raise BadRequest() - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - fc = FeaturedCollection.objects.filter( - owner=request.user, target=collection - ).first() - if fc: - fc.delete() - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - - -@login_required -def collection_share(request, collection_uuid): - collection = ( - get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if collection_uuid - else None - ) - if collection and not collection.is_visible_to(request.user): - raise PermissionDenied() - if request.method == "GET": - return render(request, "collection_share.html", {"collection": collection}) - elif request.method == "POST": - visibility = int(request.POST.get("visibility", default=0)) - comment = request.POST.get("comment") - if share_collection(collection, comment, request.user, visibility): - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - else: - return render_relogin(request) - else: - raise BadRequest() - - -def collection_retrieve_items(request, collection_uuid, edit=False, msg=None): - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - form = CollectionForm(instance=collection) - return render( - request, - "collection_items.html", - { - "collection": collection, - "form": form, - "collection_edit": edit or request.GET.get("edit"), - "msg": msg, - }, - ) - - -@login_required -def collection_append_item(request, collection_uuid): - if request.method != "POST": - raise BadRequest() - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_editable_by(request.user): - raise PermissionDenied() - - url = request.POST.get("url") - note = request.POST.get("note") - item = Item.get_by_url(url) - if item: - collection.append_item(item, note=note) - collection.save() - msg = None - else: - msg = _("条目链接无法识别,请输入本站已有条目的链接。") - return collection_retrieve_items(request, collection_uuid, True, msg) - - -@login_required -def collection_remove_item(request, collection_uuid, item_uuid): - if request.method != "POST": - raise BadRequest() - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - if not collection.is_editable_by(request.user): - raise PermissionDenied() - collection.remove_item(item) - return collection_retrieve_items(request, collection_uuid, True) - - -@login_required -def collection_move_item(request, direction, collection_uuid, item_uuid): - if request.method != "POST": - raise BadRequest() - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_editable_by(request.user): - raise PermissionDenied() - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - if direction == "up": - collection.move_up_item(item) - else: - collection.move_down_item(item) - return collection_retrieve_items(request, collection_uuid, True) - - -@login_required -def collection_update_member_order(request, collection_uuid): - if request.method != "POST": - raise BadRequest() - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_editable_by(request.user): - raise PermissionDenied() - ids = request.POST.get("member_ids", "").strip() - if not ids: - raise BadRequest() - ordered_member_ids = [int(i) for i in ids.split(",")] - collection.update_member_order(ordered_member_ids) - return collection_retrieve_items(request, collection_uuid, True) - - -@login_required -def collection_update_item_note(request, collection_uuid, item_uuid): - collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if not collection.is_editable_by(request.user): - raise PermissionDenied() - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - if not collection.is_editable_by(request.user): - raise PermissionDenied() - if request.method == "POST": - collection.update_item_metadata( - item, {"note": request.POST.get("note", default="")} - ) - return collection_retrieve_items(request, collection_uuid, True) - elif request.method == "GET": - member = collection.get_member_for_item(item) - return render( - request, - "collection_update_item_note.html", - {"collection": collection, "item": item, "note": member.note}, - ) - else: - raise BadRequest() - - -@login_required -def collection_edit(request, collection_uuid=None): - collection = ( - get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) - if collection_uuid - else None - ) - if collection and not collection.is_editable_by(request.user): - raise PermissionDenied() - if request.method == "GET": - form = CollectionForm(instance=collection) if collection else CollectionForm() - if request.GET.get("title"): - form.instance.title = request.GET.get("title") - return render( - request, - "collection_edit.html", - { - "form": form, - "collection": collection, - "user": collection.owner if collection else request.user, - }, - ) - elif request.method == "POST": - form = ( - CollectionForm(request.POST, request.FILES, instance=collection) - if collection - else CollectionForm(request.POST) - ) - if form.is_valid(): - if not collection: - form.instance.owner = request.user - form.instance.edited_time = timezone.now() - form.save() - return redirect( - reverse("journal:collection_retrieve", args=[form.instance.uuid]) - ) - else: - raise BadRequest() - else: - raise BadRequest() - - -def review_retrieve(request, review_uuid): - # piece = get_object_or_404(Review, uid=get_uuid_or_404(review_uuid)) - piece = Review.get_by_url(review_uuid) - if piece is None: - raise Http404() - if not piece.is_visible_to(request.user): - raise PermissionDenied() - return render(request, "review.html", {"review": piece}) - - -@login_required -def review_edit(request, item_uuid, review_uuid=None): - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - review = ( - get_object_or_404(Review, uid=get_uuid_or_404(review_uuid)) - if review_uuid - else None - ) - if review and not review.is_editable_by(request.user): - raise PermissionDenied() - if request.method == "GET": - form = ( - ReviewForm(instance=review) - if review - else ReviewForm(initial={"item": item.id, "share_to_mastodon": True}) - ) - return render( - request, - "review_edit.html", - { - "form": form, - "item": item, - "date_today": timezone.localdate().isoformat(), - }, - ) - elif request.method == "POST": - form = ( - ReviewForm(request.POST, instance=review) - if review - else ReviewForm(request.POST) - ) - if form.is_valid(): - mark_date = None - if request.POST.get("mark_anotherday"): - dt = parse_datetime(request.POST.get("mark_date") + " 20:00:00") - mark_date = ( - dt.replace(tzinfo=timezone.get_current_timezone()) if dt else None - ) - body = form.instance.body - if request.POST.get("leading_space"): - body = convert_leading_space_in_md(body) - review = Review.review_item_by_user( - item, - request.user, - form.cleaned_data["title"], - body, - form.cleaned_data["visibility"], - mark_date, - form.cleaned_data["share_to_mastodon"], - ) - if not review: - raise BadRequest() - return redirect(reverse("journal:review_retrieve", args=[review.uuid])) - else: - raise BadRequest() - else: - raise BadRequest() - - -@login_required -def piece_delete(request, piece_uuid): - piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid)) - return_url = request.GET.get("return_url", None) or "/" - if not piece.is_editable_by(request.user): - raise PermissionDenied() - if request.method == "GET": - return render( - request, "piece_delete.html", {"piece": piece, "return_url": return_url} - ) - elif request.method == "POST": - piece.delete() - return redirect(return_url) - else: - raise BadRequest() - - -def render_list_not_fount(request): - msg = _("相关列表不存在") - return render( - request, - "common/error.html", - { - "msg": msg, - }, - ) - - -def _render_list( - request, user_name, type, shelf_type=None, item_category=None, tag_title=None -): - user = User.get(user_name) - if user is None: - return render_user_not_found(request) - if user != request.user and ( - request.user.is_blocked_by(user) or request.user.is_blocking(user) - ): - return render_user_blocked(request) - tag = None - if type == "mark": - queryset = user.shelf_manager.get_latest_members(shelf_type, item_category) - elif type == "tagmember": - tag = Tag.objects.filter(owner=user, title=tag_title).first() - if not tag: - return render_list_not_fount(request) - if tag.visibility != 0 and user != request.user: - return render_list_not_fount(request) - queryset = TagMember.objects.filter(parent=tag) - elif type == "review": - queryset = Review.objects.filter(owner=user) - queryset = queryset.filter(query_item_category(item_category)) - else: - raise BadRequest() - queryset = queryset.filter(q_visible_to(request.user, user)).order_by( - "-created_time" - ) - paginator = Paginator(queryset, PAGE_SIZE) - page_number = request.GET.get("page", default=1) - members = paginator.get_page(page_number) - pagination = PageLinksGenerator(PAGE_SIZE, page_number, paginator.num_pages) - return render( - request, - f"user_{type}_list.html", - {"user": user, "members": members, "tag": tag, "pagination": pagination}, - ) - - -@login_required -def user_mark_list(request, user_name, shelf_type, item_category): - return _render_list( - request, user_name, "mark", shelf_type=shelf_type, item_category=item_category - ) - - -@login_required -def user_tag_member_list(request, user_name, tag_title): - return _render_list(request, user_name, "tagmember", tag_title=tag_title) - - -@login_required -def user_tag_edit(request): - if request.method == "GET": - tag_title = Tag.cleanup_title(request.GET.get("tag", ""), replace=False) - if not tag_title: - raise Http404() - tag = Tag.objects.filter(owner=request.user, title=tag_title).first() - if not tag: - raise Http404() - return render(request, "tag_edit.html", {"tag": tag}) - elif request.method == "POST": - tag_title = Tag.cleanup_title(request.POST.get("title", ""), replace=False) - tag_id = request.POST.get("id") - tag = ( - Tag.objects.filter(owner=request.user, id=tag_id).first() - if tag_id - else None - ) - if not tag or not tag_title: - msg.error(request.user, _("无效标签")) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - if request.POST.get("delete"): - tag.delete() - msg.info(request.user, _("标签已删除")) - return redirect( - reverse("journal:user_tag_list", args=[request.user.mastodon_acct]) - ) - elif ( - tag_title != tag.title - and Tag.objects.filter(owner=request.user, title=tag_title).exists() - ): - msg.error(request.user, _("标签已存在")) - return HttpResponseRedirect(request.META.get("HTTP_REFERER")) - tag.title = tag_title - tag.visibility = int(request.POST.get("visibility", 0)) - tag.visibility = 0 if tag.visibility == 0 else 2 - tag.save() - msg.info(request.user, _("标签已修改")) - return redirect( - reverse( - "journal:user_tag_member_list", - args=[request.user.mastodon_acct, tag.title], - ) - ) - raise BadRequest() - - -@login_required -def user_review_list(request, user_name, item_category): - return _render_list(request, user_name, "review", item_category=item_category) - - -@login_required -def user_tag_list(request, user_name): - user = User.get(user_name) - if user is None: - return render_user_not_found(request) - if user != request.user and ( - request.user.is_blocked_by(user) or request.user.is_blocking(user) - ): - return render_user_blocked(request) - tags = Tag.objects.filter(owner=user) - if user != request.user: - tags = tags.filter(visibility=0) - tags = tags.values("title").annotate(total=Count("members")).order_by("-total") - return render( - request, - "user_tag_list.html", - { - "user": user, - "tags": tags, - }, - ) - - -@login_required -def user_collection_list(request, user_name): - user = User.get(user_name) - if user is None: - return render_user_not_found(request) - if user != request.user and ( - request.user.is_blocked_by(user) or request.user.is_blocking(user) - ): - return render_user_blocked(request) - collections = Collection.objects.filter(owner=user) - if user != request.user: - if request.user.is_following(user): - collections = collections.filter(visibility__in=[0, 1]) - else: - collections = collections.filter(visibility=0) - return render( - request, - "user_collection_list.html", - { - "user": user, - "collections": collections, - }, - ) - - -@login_required -def user_liked_collection_list(request, user_name): - user = User.get(user_name) - if user is None: - return render_user_not_found(request) - if user != request.user and ( - request.user.is_blocked_by(user) or request.user.is_blocking(user) - ): - return render_user_blocked(request) - collections = Collection.objects.filter(likes__owner=user) - if user != request.user: - collections = collections.filter(query_visible(request.user)) - return render( - request, - "user_collection_list.html", - { - "user": user, - "collections": collections, - "liked": True, - }, - ) - - -def profile(request, user_name): - if request.method != "GET": - raise BadRequest() - user = User.get(user_name, case_sensitive=True) - if user is None or not user.is_active: - return render_user_not_found(request) - if user.mastodon_acct != user_name and user.username != user_name: - return redirect(user.url) - if not request.user.is_authenticated and user.preference.no_anonymous_view: - return render(request, "users/home_anonymous.html", {"user": user}) - if user != request.user and ( - user.is_blocked_by(request.user) or user.is_blocking(request.user) - ): - return render_user_blocked(request) - - qv = q_visible_to(request.user, user) - shelf_list = {} - visbile_categories = [ - ItemCategory.Book, - ItemCategory.Movie, - ItemCategory.TV, - ItemCategory.Music, - ItemCategory.Podcast, - ItemCategory.Game, - ItemCategory.Performance, - ] - for category in visbile_categories: - shelf_list[category] = {} - for shelf_type in ShelfType: - label = user.shelf_manager.get_label(shelf_type, category) - if label is not None: - members = user.shelf_manager.get_latest_members( - shelf_type, category - ).filter(qv) - shelf_list[category][shelf_type] = { - "title": label, - "count": members.count(), - "members": members[:10].prefetch_related("item"), - } - reviews = ( - Review.objects.filter(owner=user) - .filter(qv) - .filter(query_item_category(category)) - .order_by("-created_time") - ) - shelf_list[category]["reviewed"] = { - "title": "评论过的" + category.label, - "count": reviews.count(), - "members": reviews[:10].prefetch_related("item"), - } - collections = ( - Collection.objects.filter(owner=user).filter(qv).order_by("-created_time") - ) - liked_collections = ( - Like.user_likes_by_class(user, Collection) - .order_by("-edited_time") - .values_list("target_id", flat=True) - ) - if user != request.user: - liked_collections = liked_collections.filter(query_visible(request.user)) - top_tags = user.tag_manager.public_tags[:10] - else: - top_tags = user.tag_manager.all_tags[:10] - return render( - request, - "profile.html", - { - "user": user, - "top_tags": top_tags, - "shelf_list": shelf_list, - "collections": collections[:10], - "collections_count": collections.count(), - "liked_collections": [ - Collection.objects.get(id=i) - for i in liked_collections.order_by("-edited_time")[:10] - ], - "liked_collections_count": liked_collections.count(), - "layout": user.preference.profile_layout, - }, - ) - - -def user_calendar_data(request, user_name): - if request.method != "GET": - raise BadRequest() - user = User.get(user_name) - if user is None or not request.user.is_authenticated: - return HttpResponse("") - max_visiblity = max_visiblity_to(request.user, user) - calendar_data = user.shelf_manager.get_calendar_data(max_visiblity) - return render( - request, - "calendar_data.html", - { - "calendar_data": calendar_data, - }, - ) diff --git a/journal/views/__init__.py b/journal/views/__init__.py new file mode 100644 index 00000000..759efc54 --- /dev/null +++ b/journal/views/__init__.py @@ -0,0 +1,30 @@ +from .collection import ( + add_to_collection, + collection_add_featured, + collection_append_item, + collection_edit, + collection_move_item, + collection_remove_featured, + collection_remove_item, + collection_retrieve, + collection_retrieve_items, + collection_share, + collection_update_item_note, + collection_update_member_order, + user_collection_list, + user_liked_collection_list, +) +from .common import piece_delete +from .mark import ( + comment, + like, + mark, + mark_log, + share_comment, + unlike, + user_mark_list, + wish, +) +from .profile import profile, user_calendar_data +from .review import ReviewFeed, review_edit, review_retrieve, user_review_list +from .tag import user_tag_edit, user_tag_list, user_tag_member_list diff --git a/journal/views/collection.py b/journal/views/collection.py new file mode 100644 index 00000000..6519498e --- /dev/null +++ b/journal/views/collection.py @@ -0,0 +1,330 @@ +from django.contrib.auth.decorators import login_required +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from catalog.models import * +from common.utils import PageLinksGenerator, get_uuid_or_404 +from journal.models.renderers import convert_leading_space_in_md +from mastodon.api import share_collection +from users.models import User +from users.views import render_user_blocked, render_user_not_found + +from ..forms import * +from ..models import * +from .common import render_relogin + + +@login_required +def add_to_collection(request, item_uuid): + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if request.method == "GET": + collections = Collection.objects.filter(owner=request.user) + return render( + request, + "add_to_collection.html", + { + "item": item, + "collections": collections, + }, + ) + else: + cid = int(request.POST.get("collection_id", default=0)) + if not cid: + cid = Collection.objects.create( + owner=request.user, title=f"{request.user.display_name}的收藏单" + ).id + collection = Collection.objects.get(owner=request.user, id=cid) + collection.append_item(item, note=request.POST.get("note")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +def collection_retrieve(request, collection_uuid): + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_visible_to(request.user): + raise PermissionDenied() + follower_count = collection.likes.all().count() + following = ( + Like.user_liked_piece(request.user, collection) + if request.user.is_authenticated + else False + ) + featured_since = ( + collection.featured_by_user_since(request.user) + if request.user.is_authenticated + else None + ) + available_as_featured = ( + request.user.is_authenticated + and (following or request.user == collection.owner) + and not featured_since + and collection.members.all().exists() + ) + stats = {} + if featured_since: + stats = collection.get_stats_for_user(request.user) + stats["wishlist_deg"] = ( + round(stats["wishlist"] / stats["total"] * 360) if stats["total"] else 0 + ) + stats["progress_deg"] = ( + round(stats["progress"] / stats["total"] * 360) if stats["total"] else 0 + ) + stats["complete_deg"] = ( + round(stats["complete"] / stats["total"] * 360) if stats["total"] else 0 + ) + return render( + request, + "collection.html", + { + "collection": collection, + "follower_count": follower_count, + "following": following, + "stats": stats, + "available_as_featured": available_as_featured, + "featured_since": featured_since, + }, + ) + + +@login_required +def collection_add_featured(request, collection_uuid): + if request.method != "POST": + raise BadRequest() + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_visible_to(request.user): + raise PermissionDenied() + FeaturedCollection.objects.update_or_create(owner=request.user, target=collection) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +@login_required +def collection_remove_featured(request, collection_uuid): + if request.method != "POST": + raise BadRequest() + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_visible_to(request.user): + raise PermissionDenied() + fc = FeaturedCollection.objects.filter( + owner=request.user, target=collection + ).first() + if fc: + fc.delete() + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +@login_required +def collection_share(request, collection_uuid): + collection = ( + get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if collection_uuid + else None + ) + if collection and not collection.is_visible_to(request.user): + raise PermissionDenied() + if request.method == "GET": + return render(request, "collection_share.html", {"collection": collection}) + elif request.method == "POST": + visibility = int(request.POST.get("visibility", default=0)) + comment = request.POST.get("comment") + if share_collection(collection, comment, request.user, visibility): + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + return render_relogin(request) + else: + raise BadRequest() + + +def collection_retrieve_items(request, collection_uuid, edit=False, msg=None): + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_visible_to(request.user): + raise PermissionDenied() + form = CollectionForm(instance=collection) + return render( + request, + "collection_items.html", + { + "collection": collection, + "form": form, + "collection_edit": edit or request.GET.get("edit"), + "msg": msg, + }, + ) + + +@login_required +def collection_append_item(request, collection_uuid): + if request.method != "POST": + raise BadRequest() + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_editable_by(request.user): + raise PermissionDenied() + + url = request.POST.get("url") + note = request.POST.get("note") + item = Item.get_by_url(url) + if item: + collection.append_item(item, note=note) + collection.save() + msg = None + else: + msg = _("条目链接无法识别,请输入本站已有条目的链接。") + return collection_retrieve_items(request, collection_uuid, True, msg) + + +@login_required +def collection_remove_item(request, collection_uuid, item_uuid): + if request.method != "POST": + raise BadRequest() + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if not collection.is_editable_by(request.user): + raise PermissionDenied() + collection.remove_item(item) + return collection_retrieve_items(request, collection_uuid, True) + + +@login_required +def collection_move_item(request, direction, collection_uuid, item_uuid): + if request.method != "POST": + raise BadRequest() + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_editable_by(request.user): + raise PermissionDenied() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if direction == "up": + collection.move_up_item(item) + else: + collection.move_down_item(item) + return collection_retrieve_items(request, collection_uuid, True) + + +@login_required +def collection_update_member_order(request, collection_uuid): + if request.method != "POST": + raise BadRequest() + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_editable_by(request.user): + raise PermissionDenied() + ids = request.POST.get("member_ids", "").strip() + if not ids: + raise BadRequest() + ordered_member_ids = [int(i) for i in ids.split(",")] + collection.update_member_order(ordered_member_ids) + return collection_retrieve_items(request, collection_uuid, True) + + +@login_required +def collection_update_item_note(request, collection_uuid, item_uuid): + collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if not collection.is_editable_by(request.user): + raise PermissionDenied() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if not collection.is_editable_by(request.user): + raise PermissionDenied() + if request.method == "POST": + collection.update_item_metadata( + item, {"note": request.POST.get("note", default="")} + ) + return collection_retrieve_items(request, collection_uuid, True) + elif request.method == "GET": + member = collection.get_member_for_item(item) + return render( + request, + "collection_update_item_note.html", + {"collection": collection, "item": item, "note": member.note}, + ) + else: + raise BadRequest() + + +@login_required +def collection_edit(request, collection_uuid=None): + collection = ( + get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid)) + if collection_uuid + else None + ) + if collection and not collection.is_editable_by(request.user): + raise PermissionDenied() + if request.method == "GET": + form = CollectionForm(instance=collection) if collection else CollectionForm() + if request.GET.get("title"): + form.instance.title = request.GET.get("title") + return render( + request, + "collection_edit.html", + { + "form": form, + "collection": collection, + "user": collection.owner if collection else request.user, + }, + ) + elif request.method == "POST": + form = ( + CollectionForm(request.POST, request.FILES, instance=collection) + if collection + else CollectionForm(request.POST) + ) + if form.is_valid(): + if not collection: + form.instance.owner = request.user + form.instance.edited_time = timezone.now() + form.save() + return redirect( + reverse("journal:collection_retrieve", args=[form.instance.uuid]) + ) + else: + raise BadRequest() + else: + raise BadRequest() + + +@login_required +def user_collection_list(request, user_name): + user = User.get(user_name) + if user is None: + return render_user_not_found(request) + if user != request.user and ( + request.user.is_blocked_by(user) or request.user.is_blocking(user) + ): + return render_user_blocked(request) + collections = Collection.objects.filter(owner=user) + if user != request.user: + if request.user.is_following(user): + collections = collections.filter(visibility__in=[0, 1]) + else: + collections = collections.filter(visibility=0) + return render( + request, + "user_collection_list.html", + { + "user": user, + "collections": collections, + }, + ) + + +@login_required +def user_liked_collection_list(request, user_name): + user = User.get(user_name) + if user is None: + return render_user_not_found(request) + if user != request.user and ( + request.user.is_blocked_by(user) or request.user.is_blocking(user) + ): + return render_user_blocked(request) + collections = Collection.objects.filter(likes__owner=user) + if user != request.user: + collections = collections.filter(query_visible(request.user)) + return render( + request, + "user_collection_list.html", + { + "user": user, + "collections": collections, + "liked": True, + }, + ) diff --git a/journal/views/common.py b/journal/views/common.py new file mode 100644 index 00000000..cb36aa36 --- /dev/null +++ b/journal/views/common.py @@ -0,0 +1,97 @@ +from django.contrib.auth.decorators import login_required +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.core.paginator import Paginator +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from catalog.models import * +from common.utils import PageLinksGenerator, get_uuid_or_404 +from users.models import User +from users.views import render_user_blocked, render_user_not_found + +from ..forms import * +from ..models import * + +PAGE_SIZE = 10 + + +def render_relogin(request): + return render( + request, + "common/error.html", + { + "url": reverse("users:connect") + "?domain=" + request.user.mastodon_site, + "msg": _("信息已保存,但是未能分享到联邦宇宙"), + "secondary_msg": _( + "可能是你在联邦宇宙(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦宇宙重新登录😼" + ), + }, + ) + + +def render_list_not_found(request): + msg = _("相关列表不存在") + return render( + request, + "common/error.html", + { + "msg": msg, + }, + ) + + +def render_list( + request, user_name, type, shelf_type=None, item_category=None, tag_title=None +): + user = User.get(user_name) + if user is None: + return render_user_not_found(request) + if user != request.user and ( + request.user.is_blocked_by(user) or request.user.is_blocking(user) + ): + return render_user_blocked(request) + tag = None + if type == "mark": + queryset = user.shelf_manager.get_latest_members(shelf_type, item_category) + elif type == "tagmember": + tag = Tag.objects.filter(owner=user, title=tag_title).first() + if not tag: + return render_list_not_found(request) + if tag.visibility != 0 and user != request.user: + return render_list_not_found(request) + queryset = TagMember.objects.filter(parent=tag) + elif type == "review": + queryset = Review.objects.filter(owner=user) + queryset = queryset.filter(query_item_category(item_category)) + else: + raise BadRequest() + queryset = queryset.filter(q_visible_to(request.user, user)).order_by( + "-created_time" + ) + paginator = Paginator(queryset, PAGE_SIZE) + page_number = request.GET.get("page", default=1) + members = paginator.get_page(page_number) + pagination = PageLinksGenerator(PAGE_SIZE, page_number, paginator.num_pages) + return render( + request, + f"user_{type}_list.html", + {"user": user, "members": members, "tag": tag, "pagination": pagination}, + ) + + +@login_required +def piece_delete(request, piece_uuid): + piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid)) + return_url = request.GET.get("return_url", None) or "/" + if not piece.is_editable_by(request.user): + raise PermissionDenied() + if request.method == "GET": + return render( + request, "piece_delete.html", {"piece": piece, "return_url": return_url} + ) + elif request.method == "POST": + piece.delete() + return redirect(return_url) + else: + raise BadRequest() diff --git a/journal/views/mark.py b/journal/views/mark.py new file mode 100644 index 00000000..b121e89d --- /dev/null +++ b/journal/views/mark.py @@ -0,0 +1,313 @@ +import logging +from datetime import datetime + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils import timezone +from django.utils.dateparse import parse_datetime +from django.utils.translation import gettext_lazy as _ + +from catalog.models import * +from common.utils import PageLinksGenerator, get_uuid_or_404 +from mastodon.api import ( + get_spoiler_text, + get_status_id_by_url, + get_visibility, + post_toot, +) + +from ..forms import * +from ..models import * +from .common import render_list, render_relogin + +_logger = logging.getLogger(__name__) +PAGE_SIZE = 10 + +_checkmark = "✔️".encode("utf-8") + + +@login_required +def wish(request, item_uuid): + if request.method != "POST": + raise BadRequest() + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if not item: + raise Http404() + request.user.shelf_manager.move_item(item, ShelfType.WISHLIST) + if request.GET.get("back"): + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + return HttpResponse(_checkmark) + + +@login_required +def like(request, piece_uuid): + if request.method != "POST": + raise BadRequest() + piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid)) + if not piece: + raise Http404() + Like.user_like_piece(request.user, piece) + if request.GET.get("back"): + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + elif request.GET.get("stats"): + return render( + request, + "like_stats.html", + { + "piece": piece, + "liked": True, + "label": request.GET.get("label"), + "icon": request.GET.get("icon"), + }, + ) + return HttpResponse(_checkmark) + + +@login_required +def unlike(request, piece_uuid): + if request.method != "POST": + raise BadRequest() + piece = get_object_or_404(Piece, uid=get_uuid_or_404(piece_uuid)) + if not piece: + raise Http404() + Like.user_unlike_piece(request.user, piece) + if request.GET.get("back"): + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + elif request.GET.get("stats"): + return render( + request, + "like_stats.html", + { + "piece": piece, + "liked": False, + "label": request.GET.get("label"), + "icon": request.GET.get("icon"), + }, + ) + return HttpResponse(_checkmark) + + +@login_required +def mark(request, item_uuid): + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + mark = Mark(request.user, item) + if request.method == "GET": + tags = TagManager.get_item_tags_by_user(item, request.user) + shelf_types = [ + (n[1], n[2]) for n in iter(ShelfTypeNames) if n[0] == item.category + ] + shelf_type = request.GET.get("shelf_type", mark.shelf_type) + return render( + request, + "mark.html", + { + "item": item, + "mark": mark, + "shelf_type": shelf_type, + "tags": ",".join(tags), + "shelf_types": shelf_types, + "date_today": timezone.localdate().isoformat(), + }, + ) + elif request.method == "POST": + if request.POST.get("delete", default=False): + silence = request.POST.get("silence", False) + mark.delete(silence=silence) + if ( + silence + ): # this means the mark is deleted from mark_history, thus redirect to item page + return redirect( + reverse("catalog:retrieve", args=[item.url_path, item.uuid]) + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + visibility = int(request.POST.get("visibility", default=0)) + rating_grade = request.POST.get("rating_grade", default=0) + rating_grade = int(rating_grade) if rating_grade else None + status = ShelfType(request.POST.get("status")) + text = request.POST.get("text") + tags = request.POST.get("tags") + tags = tags.split(",") if tags else [] + share_to_mastodon = bool( + request.POST.get("share_to_mastodon", default=False) + ) + mark_date = None + if request.POST.get("mark_anotherday"): + dt = parse_datetime(request.POST.get("mark_date", "") + " 20:00:00") + mark_date = ( + dt.replace(tzinfo=timezone.get_current_timezone()) if dt else None + ) + if mark_date and mark_date >= timezone.now(): + mark_date = None + TagManager.tag_item_by_user(item, request.user, tags, visibility) + try: + mark.update( + status, + text, + rating_grade, + visibility, + share_to_mastodon=share_to_mastodon, + created_time=mark_date, + ) + except PermissionDenied as e: + _logger.warn(f"post to mastodon error 401 {request.user}") + return render_relogin(request) + except ValueError as e: + _logger.warn(f"post to mastodon error {e} {request.user}") + err = _("内容长度超出实例限制") if str(e) == "422" else str(e) + return render( + request, + "common/error.html", + { + "msg": _("标记已保存,但是未能分享到联邦宇宙"), + "secondary_msg": err, + }, + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + raise BadRequest() + + +def share_comment(user, item, text, visibility, shared_link=None, position=None): + post_error = False + status_id = get_status_id_by_url(shared_link) + link = ( + item.get_absolute_url_with_position(position) if position else item.absolute_url + ) + action_label = "评论" if text else "分享" + status = f"{action_label}{ItemCategory(item.category).label}《{item.display_title}》\n{link}\n\n{text}" + spoiler, status = get_spoiler_text(status, item) + try: + response = post_toot( + user.mastodon_site, + status, + get_visibility(visibility, user), + user.mastodon_token, + False, + status_id, + spoiler, + ) + if response and response.status_code in [200, 201]: + j = response.json() + if "url" in j: + shared_link = j["url"] + except Exception as e: + if settings.DEBUG: + raise + post_error = True + return post_error, shared_link + + +@login_required +def mark_log(request, item_uuid, log_id): + """ + Delete log of one item by log id. + """ + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + mark = Mark(request.user, item) + if request.method == "POST": + if request.GET.get("delete", default=False): + if log_id: + mark.delete_log(log_id) + else: + mark.delete_all_logs() + return render(request, "_item_user_mark_history.html", {"mark": mark}) + raise BadRequest() + + +@login_required +def comment(request, item_uuid): + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + if not item.class_name in ["podcastepisode", "tvepisode"]: + raise BadRequest("不支持评论此类型的条目") + # episode = None + # if item.class_name == "tvseason": + # try: + # episode = int(request.POST.get("episode", 0)) + # except: + # episode = 0 + # if episode <= 0: + # raise BadRequest("请输入正确的集数") + comment = Comment.objects.filter(owner=request.user, item=item).first() + if request.method == "GET": + return render( + request, + f"comment.html", + { + "item": item, + "comment": comment, + }, + ) + elif request.method == "POST": + if request.POST.get("delete", default=False): + if not comment: + raise Http404() + comment.delete() + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + visibility = int(request.POST.get("visibility", default=0)) + text = request.POST.get("text") + position = None + if item.class_name == "podcastepisode": + position = request.POST.get("position") or "0:0:0" + try: + pos = datetime.strptime(position, "%H:%M:%S") + position = pos.hour * 3600 + pos.minute * 60 + pos.second + except: + if settings.DEBUG: + raise + position = None + share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False)) + shared_link = comment.metadata.get("shared_link") if comment else None + post_error = False + if share_to_mastodon and request.user.mastodon_username: + post_error, shared_link = share_comment( + request.user, item, text, visibility, shared_link, position + ) + Comment.objects.update_or_create( + owner=request.user, + item=item, + # metadata__episode=episode, + defaults={ + "text": text, + "visibility": visibility, + "metadata": { + "shared_link": shared_link, + "position": position, + }, + }, + ) + + # if comment: + # comment.visibility = visibility + # comment.text = text + # comment.metadata["position"] = position + # comment.metadata["episode"] = episode + # if shared_link: + # comment.metadata["shared_link"] = shared_link + # comment.save() + # else: + # comment = Comment.objects.create( + # owner=request.user, + # item=item, + # text=text, + # visibility=visibility, + # metadata={ + # "shared_link": shared_link, + # "position": position, + # "episode": episode, + # }, + # ) + if post_error: + return render_relogin(request) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + raise BadRequest() + + +@login_required +def user_mark_list(request, user_name, shelf_type, item_category): + return render_list( + request, user_name, "mark", shelf_type=shelf_type, item_category=item_category + ) diff --git a/journal/views/profile.py b/journal/views/profile.py new file mode 100644 index 00000000..04876050 --- /dev/null +++ b/journal/views/profile.py @@ -0,0 +1,113 @@ +from django.contrib.auth.decorators import login_required +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.translation import gettext_lazy as _ +from user_messages import api as msg + +from catalog.models import * +from users.models import User +from users.views import render_user_blocked, render_user_not_found + +from ..forms import * +from ..models import * +from .common import render_list + + +def profile(request, user_name): + if request.method != "GET": + raise BadRequest() + user = User.get(user_name, case_sensitive=True) + if user is None or not user.is_active: + return render_user_not_found(request) + if user.mastodon_acct != user_name and user.username != user_name: + return redirect(user.url) + if not request.user.is_authenticated and user.preference.no_anonymous_view: + return render(request, "users/home_anonymous.html", {"user": user}) + if user != request.user and ( + user.is_blocked_by(request.user) or user.is_blocking(request.user) + ): + return render_user_blocked(request) + + qv = q_visible_to(request.user, user) + shelf_list = {} + visbile_categories = [ + ItemCategory.Book, + ItemCategory.Movie, + ItemCategory.TV, + ItemCategory.Music, + ItemCategory.Podcast, + ItemCategory.Game, + ItemCategory.Performance, + ] + for category in visbile_categories: + shelf_list[category] = {} + for shelf_type in ShelfType: + label = user.shelf_manager.get_label(shelf_type, category) + if label is not None: + members = user.shelf_manager.get_latest_members( + shelf_type, category + ).filter(qv) + shelf_list[category][shelf_type] = { + "title": label, + "count": members.count(), + "members": members[:10].prefetch_related("item"), + } + reviews = ( + Review.objects.filter(owner=user) + .filter(qv) + .filter(query_item_category(category)) + .order_by("-created_time") + ) + shelf_list[category]["reviewed"] = { + "title": "评论过的" + category.label, + "count": reviews.count(), + "members": reviews[:10].prefetch_related("item"), + } + collections = ( + Collection.objects.filter(owner=user).filter(qv).order_by("-created_time") + ) + liked_collections = ( + Like.user_likes_by_class(user, Collection) + .order_by("-edited_time") + .values_list("target_id", flat=True) + ) + if user != request.user: + liked_collections = liked_collections.filter(query_visible(request.user)) + top_tags = user.tag_manager.public_tags[:10] + else: + top_tags = user.tag_manager.all_tags[:10] + return render( + request, + "profile.html", + { + "user": user, + "top_tags": top_tags, + "shelf_list": shelf_list, + "collections": collections[:10], + "collections_count": collections.count(), + "liked_collections": [ + Collection.objects.get(id=i) + for i in liked_collections.order_by("-edited_time")[:10] + ], + "liked_collections_count": liked_collections.count(), + "layout": user.preference.profile_layout, + }, + ) + + +def user_calendar_data(request, user_name): + if request.method != "GET": + raise BadRequest() + user = User.get(user_name) + if user is None or not request.user.is_authenticated: + return HttpResponse("") + max_visiblity = max_visiblity_to(request.user, user) + calendar_data = user.shelf_manager.get_calendar_data(max_visiblity) + return render( + request, + "calendar_data.html", + { + "calendar_data": calendar_data, + }, + ) diff --git a/journal/views/review.py b/journal/views/review.py new file mode 100644 index 00000000..52904779 --- /dev/null +++ b/journal/views/review.py @@ -0,0 +1,158 @@ +import mimetypes + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.contrib.syndication.views import Feed +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils import timezone +from django.utils.dateparse import parse_datetime +from django.utils.translation import gettext_lazy as _ + +from catalog.models import * +from common.utils import PageLinksGenerator, get_uuid_or_404 +from journal.models.renderers import convert_leading_space_in_md, render_md +from users.models import User + +from ..forms import * +from ..models import * +from .common import render_list + + +def review_retrieve(request, review_uuid): + # piece = get_object_or_404(Review, uid=get_uuid_or_404(review_uuid)) + piece = Review.get_by_url(review_uuid) + if piece is None: + raise Http404() + if not piece.is_visible_to(request.user): + raise PermissionDenied() + return render(request, "review.html", {"review": piece}) + + +@login_required +def review_edit(request, item_uuid, review_uuid=None): + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + review = ( + get_object_or_404(Review, uid=get_uuid_or_404(review_uuid)) + if review_uuid + else None + ) + if review and not review.is_editable_by(request.user): + raise PermissionDenied() + if request.method == "GET": + form = ( + ReviewForm(instance=review) + if review + else ReviewForm(initial={"item": item.id, "share_to_mastodon": True}) + ) + return render( + request, + "review_edit.html", + { + "form": form, + "item": item, + "date_today": timezone.localdate().isoformat(), + }, + ) + elif request.method == "POST": + form = ( + ReviewForm(request.POST, instance=review) + if review + else ReviewForm(request.POST) + ) + if form.is_valid(): + mark_date = None + if request.POST.get("mark_anotherday"): + dt = parse_datetime(request.POST.get("mark_date") + " 20:00:00") + mark_date = ( + dt.replace(tzinfo=timezone.get_current_timezone()) if dt else None + ) + body = form.instance.body + if request.POST.get("leading_space"): + body = convert_leading_space_in_md(body) + review = Review.review_item_by_user( + item, + request.user, + form.cleaned_data["title"], + body, + form.cleaned_data["visibility"], + mark_date, + form.cleaned_data["share_to_mastodon"], + ) + if not review: + raise BadRequest() + return redirect(reverse("journal:review_retrieve", args=[review.uuid])) + else: + raise BadRequest() + else: + raise BadRequest() + + +@login_required +def user_review_list(request, user_name, item_category): + return render_list(request, user_name, "review", item_category=item_category) + + +MAX_ITEM_PER_TYPE = 10 + + +class ReviewFeed(Feed): + def get_object(self, request, id): + return User.get(id) + + def title(self, user): + return "%s的评论" % user.display_name if user else "无效链接" + + def link(self, user): + return user.url if user else settings.SITE_INFO["site_url"] + + def description(self, user): + return "%s的评论合集 - NeoDB" % user.display_name if user else "无效链接" + + def items(self, user): + if user is None or user.preference.no_anonymous_view: + return [] + reviews = Review.objects.filter(owner=user, visibility=0)[:MAX_ITEM_PER_TYPE] + return reviews + + def item_title(self, item: Review): + return f"{item.title} - 评论《{item.item.title}》" + + def item_description(self, item: Review): + target_html = ( + f'\n' + ) + html = render_md(item.body) + return target_html + html + + # item_link is only needed if NewsItem has no get_absolute_url method. + def item_link(self, item: Review): + return item.absolute_url + + def item_categories(self, item): + return [item.item.category.label] + + def item_pubdate(self, item): + return item.created_time + + def item_updateddate(self, item): + return item.edited_time + + def item_enclosure_url(self, item): + return item.item.cover.url + + def item_enclosure_mime_type(self, item): + t, _ = mimetypes.guess_type(item.item.cover.url) + return t + + def item_enclosure_length(self, item): + try: + size = item.item.cover.file.size + except Exception: + size = None + return size + + def item_comments(self, item): + return item.absolute_url diff --git a/journal/views/tag.py b/journal/views/tag.py new file mode 100644 index 00000000..b2847349 --- /dev/null +++ b/journal/views/tag.py @@ -0,0 +1,93 @@ +from django.contrib.auth.decorators import login_required +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.db.models import Count +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from user_messages import api as msg + +from catalog.models import * +from users.models import User +from users.views import render_user_blocked, render_user_not_found + +from ..forms import * +from ..models import * +from .common import render_list + +PAGE_SIZE = 10 + + +@login_required +def user_tag_list(request, user_name): + user = User.get(user_name) + if user is None: + return render_user_not_found(request) + if user != request.user and ( + request.user.is_blocked_by(user) or request.user.is_blocking(user) + ): + return render_user_blocked(request) + tags = Tag.objects.filter(owner=user) + if user != request.user: + tags = tags.filter(visibility=0) + tags = tags.values("title").annotate(total=Count("members")).order_by("-total") + return render( + request, + "user_tag_list.html", + { + "user": user, + "tags": tags, + }, + ) + + +@login_required +def user_tag_edit(request): + if request.method == "GET": + tag_title = Tag.cleanup_title(request.GET.get("tag", ""), replace=False) + if not tag_title: + raise Http404() + tag = Tag.objects.filter(owner=request.user, title=tag_title).first() + if not tag: + raise Http404() + return render(request, "tag_edit.html", {"tag": tag}) + elif request.method == "POST": + tag_title = Tag.cleanup_title(request.POST.get("title", ""), replace=False) + tag_id = request.POST.get("id") + tag = ( + Tag.objects.filter(owner=request.user, id=tag_id).first() + if tag_id + else None + ) + if not tag or not tag_title: + msg.error(request.user, _("无效标签")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + if request.POST.get("delete"): + tag.delete() + msg.info(request.user, _("标签已删除")) + return redirect( + reverse("journal:user_tag_list", args=[request.user.mastodon_acct]) + ) + elif ( + tag_title != tag.title + and Tag.objects.filter(owner=request.user, title=tag_title).exists() + ): + msg.error(request.user, _("标签已存在")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + tag.title = tag_title + tag.visibility = int(request.POST.get("visibility", 0)) + tag.visibility = 0 if tag.visibility == 0 else 2 + tag.save() + msg.info(request.user, _("标签已修改")) + return redirect( + reverse( + "journal:user_tag_member_list", + args=[request.user.mastodon_acct, tag.title], + ) + ) + raise BadRequest() + + +@login_required +def user_tag_member_list(request, user_name, tag_title): + return render_list(request, user_name, "tagmember", tag_title=tag_title) diff --git a/pyproject.toml b/pyproject.toml index d813c914..e5a525a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,5 @@ exclude = [ "media", ".venv", ".git" ] ignore="T002,T003,H006,H019,H020,H021,H023,H030,H031" indent=2 +[tool.isort] +profile = "black" diff --git a/requirements-dev.txt b/requirements-dev.txt index d4191cc9..d8a6dce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,5 @@ black~=22.12.0 django-debug-toolbar coverage djlint~=1.32.1 +types-dateparser +types-tqdm