From de2fde302a1a5dfd48fd0fe2a1b12aa434d2e6aa Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 25 Dec 2022 13:45:24 -0500 Subject: [PATCH] new data model: mark and review pages --- catalog/static/catalog.js | 45 ++++++++++++ catalog/templates/common_libs.html | 25 +++++++ catalog/templates/edition.html | 13 +++- catalog/templates/item_base.html | 90 +++++------------------- catalog/templates/item_mark_list.html | 48 +------------ catalog/templates/item_review_list.html | 14 +--- catalog/views.py | 3 + journal/forms.py | 35 ++++++++++ journal/models.py | 13 +++- journal/templates/mark.html | 16 ++--- journal/templates/review.html | 13 ++-- journal/templates/review_delete.html | 93 +++++++++++++++++++++++++ journal/templates/review_edit.html | 73 +++++++++++++++++++ journal/urls.py | 6 +- journal/views.py | 88 +++++++++++++++++------ social/templates/feed.html | 24 +------ 16 files changed, 402 insertions(+), 197 deletions(-) create mode 100644 catalog/static/catalog.js create mode 100644 catalog/templates/common_libs.html create mode 100644 journal/forms.py create mode 100644 journal/templates/review_delete.html create mode 100644 journal/templates/review_edit.html diff --git a/catalog/static/catalog.js b/catalog/static/catalog.js new file mode 100644 index 00000000..71fac978 --- /dev/null +++ b/catalog/static/catalog.js @@ -0,0 +1,45 @@ +function catalog_init(context) { + // readonly star rating of detail display section + let ratingLabels = $("#main .rating-star", context); + $(ratingLabels).each( function(index, value) { + let ratingScore = $(this).data("rating-score") / 2; + $(this).starRating({ + initialRating: ratingScore, + readOnly: true, + }); + }); + // readonly star rating at aside section + ratingLabels = $("#aside .rating-star"), context; + $(ratingLabels).each( function(index, value) { + let ratingScore = $(this).data("rating-score") / 2; + $(this).starRating({ + initialRating: ratingScore, + readOnly: true, + starSize: 15, + }); + }); + // hide long text + $(".entity-desc__content", context).each(function() { + let copy = $(this).clone() + .addClass('entity-desc__content--folded') + .css("visibility", "hidden"); + $(this).after(copy); + if ($(this).height() > copy.height()) { + $(this).addClass('entity-desc__content--folded'); + $(this).siblings(".entity-desc__unfold-button").removeClass("entity-desc__unfold-button--hidden"); + } + copy.remove(); + }); + + // expand hidden long text + $(".entity-desc__unfold-button a", context).on('click', function() { + $(this).parent().siblings(".entity-desc__content").removeClass('entity-desc__content--folded'); + $(this).parent(".entity-desc__unfold-button").remove(); + }); +} + +$(function() { + document.body.addEventListener('htmx:load', function(evt) { + catalog_init(evt.detail.elt); + }); +}); diff --git a/catalog/templates/common_libs.html b/catalog/templates/common_libs.html new file mode 100644 index 00000000..07413a12 --- /dev/null +++ b/catalog/templates/common_libs.html @@ -0,0 +1,25 @@ +{% load static %} +{% if sentry_dsn %} + + +{% endif %} +{% if jquery %} + +{% else %} + +{% endif %} + + + + + + + + diff --git a/catalog/templates/edition.html b/catalog/templates/edition.html index 548892c5..3b497936 100644 --- a/catalog/templates/edition.html +++ b/catalog/templates/edition.html @@ -10,12 +10,21 @@ {% load strip_scheme %} {% load thumb %} +{% block opengraph %} +{% if item.author %} + +{% endif %} +{% if item.isbn %} + +{% endif %} +{% endblock %} + {% block details %}
{% if item.rating %} - - {{ item.rating }} + + {{ item.rating | floatformat:1 }} ({{ item.rating_count }}人评分) {% else %} {% trans '评分:评分人数不足' %} diff --git a/catalog/templates/item_base.html b/catalog/templates/item_base.html index 3577821b..d003ebf9 100644 --- a/catalog/templates/item_base.html +++ b/catalog/templates/item_base.html @@ -16,23 +16,15 @@ - + - {% if item.author %} - - {% endif %} - {% if item.isbn %} - - {% endif %} - + {% block opengraph %} + {% endblock %} {{ site_name }} - {% trans item.category.label %} | {{ item.title }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - + {% include "common_libs.html" with jquery=1 %} @@ -204,9 +196,10 @@ {% endif %} {% trans '修改' %} -
+ {% csrf_token %} - {% trans '删除' %} + + {% trans '删除' %}
@@ -234,26 +227,24 @@
{% trans '标记' %}{% trans item.demonstrative %}
- - - + {% for k, v in shelf_types %} + + {% endfor %}
{% endif %}
- {% if review %} -
- +
+ {% if review %} {% trans '我的评论' %} {% if review.visibility > 0 %} {% endif %} - - {% trans '编辑' %} - {% trans '删除' %} + {% trans '编辑' %} + {% trans '删除' %}
{{ review.edited_time }}
@@ -261,20 +252,14 @@ {{ review.title }} -
- {% else %} - -
-
{% trans '我的评论' %}
+ {% else %} + {% endif %}
- - {% endif %}
{% endblock %} @@ -308,44 +293,5 @@
{% include "partial/_footer.html" %}
- diff --git a/catalog/templates/item_mark_list.html b/catalog/templates/item_mark_list.html index 079264ca..5d21e563 100644 --- a/catalog/templates/item_mark_list.html +++ b/catalog/templates/item_mark_list.html @@ -13,11 +13,7 @@ {{ site_name }} - {{ item.title }}{% trans '的标记' %} - - - - - + {% include "common_libs.html" with jquery=1 %} @@ -105,47 +101,5 @@ {% include "partial/_footer.html" %} - - - - diff --git a/catalog/templates/item_review_list.html b/catalog/templates/item_review_list.html index 3822ed44..ac236955 100644 --- a/catalog/templates/item_review_list.html +++ b/catalog/templates/item_review_list.html @@ -13,11 +13,7 @@ {{ site_name }} - {{ item.title }}{% trans '的评论' %} - - - - - + {% include "common_libs.html" with jquery=1 %} @@ -30,22 +26,18 @@
- {{ item.title }}{% trans ' 的评论' %} + {{ item.title }}{% trans ' 的评论' %}
    {% for review in reviews %} -
  • - {{ review.owner.username }} {% if review.visibility > 0 %} {% endif %} {{ review.edited_time }} - - {{ review.title }} - + {{ review.title }}
  • {% empty %}
    {% trans '无结果' %}
    diff --git a/catalog/views.py b/catalog/views.py index f51191a2..956e1523 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -19,6 +19,7 @@ from journal.models import Mark, ShelfMember, Review from journal.models import query_visible, query_following from common.utils import PageLinksGenerator from common.views import PAGE_LINK_NUMBER +from journal.models import ShelfTypeNames _logger = logging.getLogger(__name__) @@ -43,6 +44,7 @@ def retrieve(request, item_path, item_uuid): mark_list = None review_list = None collection_list = [] + shelf_types = [(n[1], n[2]) for n in iter(ShelfTypeNames) if n[0] == item.category] if request.user.is_authenticated: visible = query_visible(request.user) mark = Mark(request.user, item) @@ -60,6 +62,7 @@ def retrieve(request, item_path, item_uuid): 'mark_list': mark_list, 'review_list': review_list, 'collection_list': collection_list, + 'shelf_types': shelf_types, } ) else: diff --git a/journal/forms.py b/journal/forms.py new file mode 100644 index 00000000..fdd6ae34 --- /dev/null +++ b/journal/forms.py @@ -0,0 +1,35 @@ +from django import forms +from markdownx.fields import MarkdownxFormField +import django.contrib.postgres.forms as postgres +from django.utils import formats +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +import json +from .models import * + + +class ReviewForm(forms.ModelForm): + class Meta: + model = Review + fields = [ + 'id', + 'item', + 'title', + 'body', + 'visibility' + ] + widgets = { + 'item': forms.TextInput(attrs={"hidden": ""}), + } + title = forms.CharField(label=_("评论标题")) + body = MarkdownxFormField(label=_("评论正文 (Markdown)")) + share_to_mastodon = forms.BooleanField( + label=_("分享到联邦网络"), initial=True, required=False) + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + visibility = forms.TypedChoiceField( + label=_("可见性"), + initial=0, + coerce=int, + choices=VisibilityType.choices, + widget=forms.RadioSelect + ) diff --git a/journal/models.py b/journal/models.py index 264c2347..8b71ca4e 100644 --- a/journal/models.py +++ b/journal/models.py @@ -23,6 +23,12 @@ from django.db.models import Q import mistune +class VisibilityType(models.IntegerChoices): + Public = 0, _('公开') + Follower_Only = 1, _('仅关注者') + Private = 2, _('仅自己') + + def query_visible(user): return Q(visibility=0) | Q(owner_id__in=user.following, visibility__lt=2) | Q(owner_id=user.id) @@ -47,7 +53,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): @property def url(self): - return f'/{self.url_path}/{self.uuid}/' if self.url_path else None + return f'/{self.url_path}/{self.uuid}' if self.url_path else None @property def absolute_url(self): @@ -62,7 +68,7 @@ class Content(Piece): item = models.ForeignKey(Item, on_delete=models.PROTECT) def __str__(self): - return f"{self.id}({self.item})" + return f"{self.uuid}@{self.item}" class Meta: abstract = True @@ -644,3 +650,6 @@ class Mark: if self.shelfmember.metadata.get('shared_link') != self.shared_link: self.shelfmember.metadata['shared_link'] = self.shared_link self.shelfmember.save() + + def delete(self): + self.update(None, None, None, 0) diff --git a/journal/templates/mark.html b/journal/templates/mark.html index 997cefba..63c81ea2 100644 --- a/journal/templates/mark.html +++ b/journal/templates/mark.html @@ -31,17 +31,17 @@
      {% for k, v in shelf_types %} -
    • +
    • {% endfor %}
    - +
    - +
    @@ -100,19 +100,19 @@ // hide rating star when select wish const WISH_CODE = "wishlist"; if ($("#statusSelection input[type='radio']:checked").val() == WISH_CODE) { - $("#model .rating-star-edit").hide(); + $("#modal .rating-star-edit").hide(); } $("#statusSelection input[type='radio']").on('click', function() { if ($(this).val() == WISH_CODE) { - $("#model .rating-star-edit").hide(); + $("#modal .rating-star-edit").hide(); } else { - $("#model .rating-star-edit").show(); + $("#modal .rating-star-edit").show(); } }); // show confirm modal - $("#model a.delete").on('click', function(e) { + $("#modal a.delete").on('click', function(e) { e.preventDefault(); $(".confirm-modal").show(); $(".bg-mask").show(); @@ -121,7 +121,7 @@ // confirm modal $(".confirm-modal input[type='submit']").on('click', function(e) { e.preventDefault(); - $("#model form").submit(); + $("#modal form").submit(); }); })(); diff --git a/journal/templates/review.html b/journal/templates/review.html index c90cf63e..561e6ffe 100644 --- a/journal/templates/review.html +++ b/journal/templates/review.html @@ -11,18 +11,13 @@ - + {{ site_name }}{% trans '评论' %} - {{ review.title }} - - - - - - + {% include "common_libs.html" with jquery=1 %} @@ -62,8 +57,8 @@
    {% if request.user == review.owner %} - {% trans '编辑' %} - {% trans '删除' %} + {% trans '编辑' %} + {% trans '删除' %} {% endif %}
diff --git a/journal/templates/review_delete.html b/journal/templates/review_delete.html new file mode 100644 index 00000000..a44b5791 --- /dev/null +++ b/journal/templates/review_delete.html @@ -0,0 +1,93 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {{ site_name }} - {% trans '删除评论' %} + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
{% trans '确认删除这篇评论吗?' %}
+ +
+ + +
+ +
+ {{ review.title }} +
+ {% if review.visibility > 0 %} + + + + {% endif %} + +
+
+ + {{ review.owner.username }} + + {{ review.edited_time }} + +
+
+ + +
+
+ {{ form.body }} +
+ {{ form.media }} + +
+ +
+
+ {% csrf_token %} + +
+ +
+
+
+
+ +
+ {% include "partial/_footer.html" %} +
+ + + + + + + + \ No newline at end of file diff --git a/journal/templates/review_edit.html b/journal/templates/review_edit.html new file mode 100644 index 00000000..f4e554e7 --- /dev/null +++ b/journal/templates/review_edit.html @@ -0,0 +1,73 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + + {{ site_name }} - {{ item.title }} - {% trans '评论' %} + {% include "common_libs.html" with jquery=1 %} + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+ +
+ {% csrf_token %} + {{ form.item }} +
+ {{ form.title.label }} +
+ {{ form.title }} +
+ + {{ form.body.label }} + + + {% trans '预览' %} + +
+
+ {{ form.body }} +
+
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
+
+
+ + {{ form.visibility.label }}{{ form.visibility }} +
+ +
+
+ +
+ {{ form.media }} +
+
+
+
+ {% include "sidebar_item.html" %} +
+
+
+
+ {% include "partial/_footer.html" %} +
+ + diff --git a/journal/urls.py b/journal/urls.py index 30cf8ba6..434f7796 100644 --- a/journal/urls.py +++ b/journal/urls.py @@ -9,6 +9,8 @@ urlpatterns = [ path('mark/', mark, name='mark'), path('add_to_collection/', add_to_collection, name='add_to_collection'), - path('review/', review_retrieve, name='review_retrieve'), - path('review/create', review_create, name='review_create'), + path('review/', review_retrieve, name='review_retrieve'), + path('review/create//', review_edit, name='review_create'), + path('review/edit//', review_edit, name='review_edit'), + path('review/delete/', review_delete, name='review_delete'), ] diff --git a/journal/views.py b/journal/views.py index d10d5096..91d6d14a 100644 --- a/journal/views.py +++ b/journal/views.py @@ -17,6 +17,9 @@ from django.db.models import Q import time from management.models import Announcement from django.utils.baseconv import base62 +from .forms import * +from mastodon.api import share_review + _logger = logging.getLogger(__name__) PAGE_SIZE = 10 @@ -82,43 +85,85 @@ def mark(request, item_uuid): 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, }) elif request.method == 'POST': - visibility = int(request.POST.get('visibility', default=0)) - rating = int(request.POST.get('rating', default=0)) - 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)) - TagManager.tag_item_by_user(item, request.user, tags, visibility) - try: - mark.update(status, text, rating, visibility, share_to_mastodon=share_to_mastodon) - except Exception: - go_relogin(request) - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + if request.POST.get('delete', default=False): + mark.delete() + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + else: + visibility = int(request.POST.get('visibility', default=0)) + rating = request.POST.get('rating', default=0) + rating = int(rating) if rating 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)) + TagManager.tag_item_by_user(item, request.user, tags, visibility) + try: + mark.update(status, text, rating, visibility, share_to_mastodon=share_to_mastodon) + except Exception: + go_relogin(request) + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) -def review_retrieve(request, piece_uuid): - piece = get_object_or_404(Review, uid=base62.decode(piece_uuid)) - if not piece: - return HttpResponseNotFound("piece not found") +def review_retrieve(request, review_uuid): + piece = get_object_or_404(Review, uid=base62.decode(review_uuid)) if not piece.is_visible_to(request.user): raise PermissionDenied() return render(request, 'review.html', {'review': piece}) -def review_edit(request, piece_uuid): - pass +@login_required +def review_edit(request, item_uuid, review_uuid=None): + item = get_object_or_404(Item, uid=base62.decode(item_uuid)) + review = get_object_or_404(Review, uid=base62.decode(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}) + return render(request, 'review_edit.html', {'form': form, 'item': item}) + elif request.method == 'POST': + form = ReviewForm(request.POST, instance=review) if review else ReviewForm(request.POST) + if form.is_valid(): + if not review: + form.instance.owner = request.user + form.instance.edited_time = timezone.now() + form.save() + if form.cleaned_data['share_to_mastodon']: + form.instance.save = lambda **args: None + form.instance.shared_link = None + if not share_review(form.instance): + return go_relogin(request) + return redirect(reverse("journal:review_retrieve", args=[form.instance.uuid])) + else: + return HttpResponseBadRequest(form.errors) + else: + return HttpResponseBadRequest() -def review_create(request): - pass +@login_required +def review_delete(request, review_uuid): + review = get_object_or_404(Review, uid=base62.decode(review_uuid)) + if not review.is_editable_by(request.user): + raise PermissionDenied() + if request.method == 'GET': + review_form = ReviewForm(instance=review) + return render(request, 'review_delete.html', {'form': review_form, 'review': review}) + elif request.method == 'POST': + item = review.item + print(review) + review.delete() + return redirect(item.url) + else: + return HttpResponseBadRequest() def mark_list(request, shelf_type, item_category): @@ -133,5 +178,6 @@ def collection_list(request): pass +@login_required def liked_list(request): pass diff --git a/social/templates/feed.html b/social/templates/feed.html index b9c31d81..fb32550c 100644 --- a/social/templates/feed.html +++ b/social/templates/feed.html @@ -13,29 +13,7 @@ {{ site_name }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - + {% include "common_libs.html" with jquery=1 %}