new data model: mark and review
This commit is contained in:
parent
744413b2fc
commit
116ca00a7d
13 changed files with 595 additions and 76 deletions
|
@ -227,15 +227,15 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return f'/{self.url_path}/{self.uuid}/'
|
||||
return f'/{self.url_path}/{self.uuid}' if self.url_path else None
|
||||
|
||||
@property
|
||||
def absolute_url(self):
|
||||
return settings.APP_WEBSITE + self.url
|
||||
return (settings.APP_WEBSITE + self.url) if self.url_path else None
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return '/api' + self.url
|
||||
return ('/api/' + self.url) if self.url_path else None
|
||||
|
||||
@property
|
||||
def class_name(self):
|
||||
|
|
|
@ -127,14 +127,47 @@
|
|||
|
||||
<div class="entity-marks">
|
||||
<h5 class="entity-marks__title">{% trans '标记' %}</h5>
|
||||
<a href="{% url 'books:retrieve_mark_list' item.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
|
||||
<a href="{% url 'books:retrieve_mark_list' item.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
|
||||
{% include "partial/mark_list.html" with mark_list=mark_list current_item=book %}
|
||||
{% if mark_list %}
|
||||
<a href="{% url 'catalog:mark_list' item.url_path item.uuid %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
|
||||
<a href="{% url 'catalog:mark_list' item.url_path item.uuid 'following' %}" class="entity-marks__more-link">关注的人的标记</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-marks__mark-list">
|
||||
{% for others_mark in mark_list %}
|
||||
<li class="entity-marks__mark">
|
||||
<a href="{% url 'users:home' others_mark.owner.mastodon_username %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
|
||||
|
||||
<span>{{ others_mark.shelf_label }}</span>
|
||||
|
||||
{% if others_mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% if others_mark.visibility > 0 %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
|
||||
{% if others_mark.shelfmember.metadata.shared_link %}
|
||||
<a href="{{ others_mark.shelfmember.metadata.shared_link }}" target="_blank"><span class="entity-marks__mark-time">{{ others_mark.created_time }}</span></a>
|
||||
{% else %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.created_time }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if others_mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ others_mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
|
||||
<div> {% trans '暂无标记' %} </div>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="entity-reviews">
|
||||
<h5 class="entity-reviews__title">{% trans '评论' %}</h5>
|
||||
{% if review_list_more %}
|
||||
<a href="{% url 'books:retrieve_review_list' item.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
|
||||
{% if review_list %}
|
||||
<a href="{% url 'catalog:review_list' item.url_path item.uuid %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
|
||||
{% endif %}
|
||||
{% if review_list %}
|
||||
<ul class="entity-reviews__review-list">
|
||||
|
@ -145,10 +178,7 @@
|
|||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
|
||||
{% if others_review.book != book %}
|
||||
<span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'books:retrieve' others_review.item.id %}">{{ others_review.item.get_source_site_display }}</a></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'journal:review_retrieve' others_review.uuid %}">{{ others_review.title }}</a></span>
|
||||
<span>{{ others_review.get_plain_content | truncate:100 }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -228,7 +258,7 @@
|
|||
|
||||
<div class="review-panel__time">{{ review.edited_time }}</div>
|
||||
|
||||
<a href="{% url 'books:retrieve_review' review.id %}" class="review-panel__review-title">
|
||||
<a href="{% url 'journal:review_retrieve' review.uuid %}" class="review-panel__review-title">
|
||||
{{ review.title }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
151
catalog/templates/item_mark_list.html
Normal file
151
catalog/templates/item_mark_list.html
Normal file
|
@ -0,0 +1,151 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
{% load thumb %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site_name }} - {{ item.title }}{% trans '的标记' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content">
|
||||
<div class="grid">
|
||||
<div class="grid__main" id="main">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="entity-marks">
|
||||
<h5 class="entity-marks__title entity-marks__title--stand-alone">
|
||||
<a href="{% url 'catalog:retrieve' item.url_path item.uuid %}">{{ item.title }}</a>{% trans ' 的标记' %}
|
||||
</h5>
|
||||
<ul class="entity-marks__mark-list">
|
||||
{% for others_mark in marks %}
|
||||
<li class="entity-marks__mark">
|
||||
<a href="{% url 'users:home' others_mark.owner.mastodon_username %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
|
||||
|
||||
<span>{{ others_mark.mark.get_status_display }}</span>
|
||||
|
||||
{% if others_mark.mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.mark.rating }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% if others_mark.mark.visibility > 0 %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
|
||||
{% if others_mark.metadata.shared_link %}
|
||||
<a href="{{ others_mark.metadata.shared_link }}" target="_blank"><span class="entity-marks__mark-time">{{ others_mark.mark.created_time }}</span></a>
|
||||
{% else %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.mark.created_time }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if others_mark.mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ others_mark.mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
|
||||
<div> {% trans '暂无标记' %} </div>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
|
||||
{% if marks.pagination.has_prev %}
|
||||
<a href="?page=1"
|
||||
class="pagination__nav-link pagination__nav-link">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page in marks.pagination.page_range %}
|
||||
|
||||
{% if page == marks.pagination.current_page %}
|
||||
<a href="?page={{ page }}"
|
||||
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
||||
{% else %}
|
||||
<a href="?page={{ page }}"
|
||||
class="pagination__page-link">{{ page }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% if marks.pagination.has_next %}
|
||||
<a href="?page={{ marks.next_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid__aside" id="aside">
|
||||
{% include "sidebar_item.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// readonly star rating of detail display section
|
||||
let ratingLabels = $("#main .rating-star");
|
||||
$(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");
|
||||
$(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").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").on('click', function() {
|
||||
$(this).parent().siblings(".entity-desc__content").removeClass('entity-desc__content--folded');
|
||||
$(this).parent(".entity-desc__unfold-button").remove();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
100
catalog/templates/item_review_list.html
Normal file
100
catalog/templates/item_review_list.html
Normal file
|
@ -0,0 +1,100 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
{% load thumb %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site_name }} - {{ item.title }}{% trans '的评论' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
<section id="content">
|
||||
<div class="grid">
|
||||
<div class="grid__main" id="main">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="entity-reviews">
|
||||
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
|
||||
<a href="{% url 'books:retrieve' item.id %}">{{ item.title }}</a>{% trans ' 的评论' %}
|
||||
</h5>
|
||||
<ul class="entity-reviews__review-list">
|
||||
|
||||
{% for review in reviews %}
|
||||
|
||||
<li class="entity-reviews__review entity-reviews__review--wider">
|
||||
|
||||
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.visibility > 0 %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
|
||||
|
||||
<span href="{% url 'books:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'books:retrieve_review' review.id %}">{{ review.title }}</a></span>
|
||||
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>{% trans '无结果' %}</div>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
|
||||
{% if reviews.pagination.has_prev %}
|
||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</a>
|
||||
<a href="?page={{ reviews.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page in reviews.pagination.page_range %}
|
||||
|
||||
{% if page == reviews.pagination.current_page %}
|
||||
<a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
||||
{% else %}
|
||||
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% if reviews.pagination.has_next %}
|
||||
<a href="?page={{ reviews.next_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
||||
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">»</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid__aside" id="aside">
|
||||
{% include "sidebar_item.html" %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -36,10 +36,10 @@
|
|||
{% block details %}
|
||||
<div class="entity-detail__fields">
|
||||
<div class="entity-detail__rating">
|
||||
{% if item.rating and item.rating_number >= 5 %}
|
||||
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
|
||||
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
|
||||
<small>({{ item.rating_number }}人评分)</small>
|
||||
{% if item.rating %}
|
||||
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:'0' }}"></span>
|
||||
<span class="entity-detail__rating-score"> {{ item.rating|floatformat:1 }} </span>
|
||||
<small>({{ item.rating_count }}人评分)</small>
|
||||
{% else %}
|
||||
<span> {% trans '评分:评分人数不足' %}</span>
|
||||
{% endif %}
|
||||
|
@ -156,7 +156,7 @@
|
|||
|
||||
|
||||
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
|
||||
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
|
||||
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' 'fixme' %}">{{ item.last_editor | default:"" }}</a></div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
|
|
31
catalog/templates/sidebar_item.html
Normal file
31
catalog/templates/sidebar_item.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load truncate %}
|
||||
{% load thumb %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="entity-card">
|
||||
<div class="entity-card__img-wrapper">
|
||||
<a href="{% url 'catalog:retrieve' item.url_path item.uuid %}"><img src="{{ item.cover|thumb:'normal' }}" alt="" class="entity-card__img"></a>
|
||||
</div>
|
||||
<div class="entity-card__info-wrapper">
|
||||
<h5 class="entity-card__title"><a href="{% url 'catalog:retrieve' item.url_path item.uuid %}">{{ item.title }}</a>
|
||||
{% for res in item.external_resources.all %}
|
||||
<a href="{{ res.url }}">
|
||||
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</h5>
|
||||
|
||||
{% if item.isbn %}
|
||||
<div>ISBN: {{ item.isbn }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div>{% if item.pub_house %}{% trans '出版社:' %}{{ item.pub_house }}{% endif %}</div>
|
||||
{% if item.rating %}
|
||||
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ item.rating }}"></span>
|
||||
<span class="entity-card__rating-score rating-score">{{ item.rating }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -3,6 +3,8 @@ from .api import api
|
|||
from .views import *
|
||||
from .models import *
|
||||
|
||||
app_name = 'catalog'
|
||||
|
||||
|
||||
def _get_all_url_paths():
|
||||
paths = ['item']
|
||||
|
@ -15,7 +17,9 @@ def _get_all_url_paths():
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^item/(?P<item_uuid>[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/', retrieve_by_uuid, name='retrieve_by_uuid'),
|
||||
re_path(r'^(?P<item_path>' + _get_all_url_paths() + ')/(?P<item_uid>[A-Za-z0-9]{21,22})/', retrieve, name='retrieve'),
|
||||
re_path(r'^item/(?P<item_uid>[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/?$', retrieve_by_uuid, name='retrieve_by_uuid'),
|
||||
re_path(r'^(?P<item_path>' + _get_all_url_paths() + ')/(?P<item_uuid>[A-Za-z0-9]{21,22})$', retrieve, name='retrieve'),
|
||||
re_path(r'^(?P<item_path>' + _get_all_url_paths() + ')/(?P<item_uuid>[A-Za-z0-9]{21,22})/reviews', review_list, name='review_list'),
|
||||
re_path(r'^(?P<item_path>' + _get_all_url_paths() + ')/(?P<item_uuid>[A-Za-z0-9]{21,22})/marks(?:/(?P<following_only>\\w+))?', mark_list, name='mark_list'),
|
||||
path("api/", api.urls),
|
||||
]
|
||||
|
|
|
@ -12,56 +12,100 @@ from mastodon import mastodon_request_included
|
|||
from mastodon.models import MastodonApplication
|
||||
from mastodon.api import share_mark, share_review
|
||||
from .models import *
|
||||
# from .forms import *
|
||||
# from .forms import BookMarkStatusTranslator
|
||||
from django.conf import settings
|
||||
from collection.models import CollectionItem
|
||||
from common.scraper import get_scraper_by_url, get_normalized_url
|
||||
from django.utils.baseconv import base62
|
||||
from journal.models import Mark
|
||||
from journal.models import query_visible
|
||||
|
||||
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
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def retrieve_by_uuid(request, item_uuid):
|
||||
item = get_object_or_404(Item, uid=item_uuid)
|
||||
NUM_REVIEWS_ON_ITEM_PAGE = 5
|
||||
NUM_REVIEWS_ON_LIST_PAGE = 20
|
||||
|
||||
|
||||
def retrieve_by_uuid(request, item_uid):
|
||||
item = get_object_or_404(Item, uid=item_uid)
|
||||
return redirect(item.url)
|
||||
|
||||
|
||||
def retrieve(request, item_path, item_uid):
|
||||
def retrieve(request, item_path, item_uuid):
|
||||
if request.method == 'GET':
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uid))
|
||||
item_url = f'/{item_path}/{item_uid}/'
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
item_url = f'/{item_path}/{item_uuid}'
|
||||
if item.url != item_url:
|
||||
return redirect(item.url)
|
||||
mark = None
|
||||
review = None
|
||||
mark_list = None
|
||||
review_list = None
|
||||
mark_list_more = None
|
||||
review_list_more = None
|
||||
collection_list = []
|
||||
mark_form = None
|
||||
if request.user.is_authenticated:
|
||||
visible = query_visible(request.user)
|
||||
mark = Mark(request.user, item)
|
||||
_logger.info(mark.rating)
|
||||
review = mark.review
|
||||
collection_list = item.collections.all().filter(query_visible(request.user)).annotate(like_counts=Count('likes')).order_by('-like_counts')
|
||||
collection_list = item.collections.all().filter(visible).annotate(like_counts=Count('likes')).order_by('-like_counts')
|
||||
mark_query = ShelfMember.objects.filter(item=item).filter(visible).order_by('-created_time')
|
||||
mark_list = [member.mark for member in mark_query[:NUM_REVIEWS_ON_ITEM_PAGE]]
|
||||
review_list = Review.objects.filter(item=item).filter(visible).order_by('-created_time')[:NUM_REVIEWS_ON_ITEM_PAGE]
|
||||
|
||||
return render(request, item.class_name + '.html', {
|
||||
'item': item,
|
||||
'mark': mark,
|
||||
'review': review,
|
||||
'mark_form': mark_form,
|
||||
'mark_list': mark_list,
|
||||
'mark_list_more': mark_list_more,
|
||||
'review_list': review_list,
|
||||
'review_list_more': review_list_more,
|
||||
'collection_list': collection_list,
|
||||
}
|
||||
)
|
||||
else:
|
||||
logger.warning('non-GET method at /book/<id>')
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
def mark_list(request, item_path, item_uuid, following_only=False):
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
if not item:
|
||||
return HttpResponseNotFound("item not found")
|
||||
queryset = ShelfMember.objects.filter(item=item).order_by('-created_time')
|
||||
if following_only:
|
||||
queryset = queryset.filter(query_following(request.user))
|
||||
else:
|
||||
queryset = queryset.filter(query_visible(request.user))
|
||||
paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
marks = paginator.get_page(page_number)
|
||||
marks.pagination = PageLinksGenerator(
|
||||
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
return render(
|
||||
request,
|
||||
'item_mark_list.html',
|
||||
{
|
||||
'marks': marks,
|
||||
'item': item,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def review_list(request, item_path, item_uuid):
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
if not item:
|
||||
return HttpResponseNotFound("item not found")
|
||||
queryset = Review.objects.filter(item=item).order_by('-created_time')
|
||||
queryset = queryset.filter(query_visible(request.user))
|
||||
paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
reviews = paginator.get_page(page_number)
|
||||
reviews.pagination = PageLinksGenerator(
|
||||
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
return render(
|
||||
request,
|
||||
'item_review_list.html',
|
||||
{
|
||||
'reviews': reviews,
|
||||
'item': item,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -20,13 +20,19 @@ import uuid
|
|||
from catalog.common.utils import DEFAULT_ITEM_COVER, item_cover_path
|
||||
from django.utils.baseconv import base62
|
||||
from django.db.models import Q
|
||||
import mistune
|
||||
|
||||
|
||||
def query_visible(user):
|
||||
return Q(visibility=0) | Q(owner_id__in=user.following, visibility__lt=2) | Q(owner_id=user.id)
|
||||
|
||||
|
||||
def query_following(user):
|
||||
return Q(owner_id__in=user.following, visibility__lt=2) | Q(owner_id=user.id)
|
||||
|
||||
|
||||
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||
url_path = 'piece' # subclass must specify this
|
||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
|
||||
|
@ -39,6 +45,18 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
def uuid(self):
|
||||
return base62.encode(self.uid.int)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f'/{self.url_path}/{self.uuid}/' if self.url_path else None
|
||||
|
||||
@property
|
||||
def absolute_url(self):
|
||||
return (settings.APP_WEBSITE + self.url) if self.url_path else None
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return ('/api/' + self.url) if self.url_path else None
|
||||
|
||||
|
||||
class Content(Piece):
|
||||
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
||||
|
@ -87,10 +105,15 @@ class Comment(Content):
|
|||
|
||||
|
||||
class Review(Content):
|
||||
url_path = 'review'
|
||||
title = models.CharField(max_length=500, blank=False, null=False)
|
||||
body = MarkdownxField()
|
||||
|
||||
@staticmethod
|
||||
@property
|
||||
def html_content(self):
|
||||
return mistune.html(self.body)
|
||||
|
||||
@ staticmethod
|
||||
def review_item_by_user(item, user, title, body, metadata={}, visibility=0):
|
||||
# allow multiple reviews per item per user.
|
||||
review = Review.objects.create(owner=user, item=item, title=title, body=body, metadata=metadata, visibility=visibility)
|
||||
|
@ -114,17 +137,17 @@ class Review(Content):
|
|||
class Rating(Content):
|
||||
grade = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True)
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def get_rating_for_item(item):
|
||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(average=Avg('grade'), count=Count('item'))
|
||||
return math.ceil(stat['average']) if stat['count'] >= 5 else None
|
||||
return stat['average'] if stat['count'] >= 5 else None
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def get_rating_count_for_item(item):
|
||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(count=Count('item'))
|
||||
return stat['count']
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def rate_item_by_user(item, user, rating_grade, visibility=0):
|
||||
if rating_grade and (rating_grade < 1 or rating_grade > 10):
|
||||
raise ValueError(f'Invalid rating grade: {rating_grade}')
|
||||
|
@ -141,7 +164,7 @@ class Rating(Content):
|
|||
rating.save()
|
||||
return rating
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def get_item_rating_by_user(item, user):
|
||||
rating = Rating.objects.filter(owner=user, item=item).first()
|
||||
return rating.grade if rating else None
|
||||
|
@ -180,11 +203,11 @@ class List(Piece):
|
|||
# subclass must add this:
|
||||
# items = models.ManyToManyField(Item, through='ListMember')
|
||||
|
||||
@property
|
||||
@ property
|
||||
def ordered_members(self):
|
||||
return self.members.all().order_by('position', 'item_id')
|
||||
|
||||
@property
|
||||
@ property
|
||||
def ordered_items(self):
|
||||
return self.items.all().order_by(self.MEMBER_CLASS.__name__.lower() + '__position')
|
||||
|
||||
|
@ -281,15 +304,19 @@ ShelfTypeNames = [
|
|||
[ItemCategory.Game, ShelfType.WISHLIST, _('想玩')],
|
||||
[ItemCategory.Game, ShelfType.PROGRESS, _('在玩')],
|
||||
[ItemCategory.Game, ShelfType.COMPLETE, _('玩过')],
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
class ShelfMember(ListMember):
|
||||
parent = models.ForeignKey('Shelf', related_name='members', on_delete=models.CASCADE)
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def mark(self):
|
||||
return Mark(self.owner, self.item)
|
||||
m = Mark(self.owner, self.item)
|
||||
m.shelfmember = self
|
||||
return m
|
||||
|
||||
|
||||
class Shelf(List):
|
||||
|
@ -304,11 +331,11 @@ class Shelf(List):
|
|||
def __str__(self):
|
||||
return f'{self.id} {self.title}'
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def shelf_label(self):
|
||||
return next(iter([n[2] for n in iter(ShelfTypeNames) if n[0] == self.item_category and n[1] == self.shelf_type]), self.shelf_type)
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def title(self):
|
||||
q = _("{item_category} {shelf_label} list").format(shelf_label=self.shelf_label, item_category=self.item_category)
|
||||
return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
||||
|
@ -400,7 +427,7 @@ class ShelfManager:
|
|||
def get_shelf(self, item_category, shelf_type):
|
||||
return self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first()
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def get_manager_for_user(user):
|
||||
return ShelfManager(user)
|
||||
|
||||
|
@ -427,7 +454,7 @@ class Collection(List):
|
|||
items = models.ManyToManyField(Item, through='CollectionMember', related_name="collections")
|
||||
collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers
|
||||
|
||||
@property
|
||||
@ property
|
||||
def plain_description(self):
|
||||
html = markdown(self.description)
|
||||
return RE_HTML_TAG.sub(' ', html)
|
||||
|
@ -465,23 +492,23 @@ class Tag(List):
|
|||
class Meta:
|
||||
unique_together = [['_owner', 'title']]
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def cleanup_title(title):
|
||||
return title.strip().lower()
|
||||
|
||||
|
||||
class TagManager:
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def public_tags_for_item(item):
|
||||
tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')
|
||||
tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')[: 20]
|
||||
return sorted(list(map(lambda t: t['title'], tags)))
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def all_tags_for_user(user):
|
||||
tags = user.tag_set.all().values('title').annotate(frequency=Count('members')).order_by('-frequency')
|
||||
return sorted(list(map(lambda t: t['title'], tags)))
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def tag_item_by_user(item, user, tag_titles, default_visibility=0):
|
||||
titles = set([Tag.cleanup_title(tag_title) for tag_title in tag_titles])
|
||||
current_titles = set([m.parent.title for m in TagMember.objects.filter(owner=user, item=item)])
|
||||
|
@ -494,12 +521,12 @@ class TagManager:
|
|||
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||
tag.remove_item(item)
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def get_item_tags_by_user(item, user):
|
||||
current_titles = [m.parent.title for m in TagMember.objects.filter(owner=user, item=item)]
|
||||
return current_titles
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def add_tag_by_user(item, tag_title, user, default_visibility=0):
|
||||
title = Tag.cleanup_title(tag_title)
|
||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||
|
@ -507,14 +534,14 @@ class TagManager:
|
|||
tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility)
|
||||
tag.append_item(item)
|
||||
|
||||
@staticmethod
|
||||
@ staticmethod
|
||||
def get_manager_for_user(user):
|
||||
return TagManager(user)
|
||||
|
||||
def __init__(self, user):
|
||||
self.owner = user
|
||||
|
||||
@property
|
||||
@ property
|
||||
def all_tags(self):
|
||||
return TagManager.all_tags_for_user(self.owner)
|
||||
|
||||
|
@ -539,55 +566,55 @@ class Mark:
|
|||
self.owner = user
|
||||
self.item = item
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def shelfmember(self):
|
||||
return self.owner.shelf_manager.locate_item(self.item)
|
||||
|
||||
@property
|
||||
@ property
|
||||
def id(self):
|
||||
return self.shelfmember.id if self.shelfmember else None
|
||||
|
||||
@property
|
||||
@ property
|
||||
def shelf(self):
|
||||
return self.shelfmember.parent if self.shelfmember else None
|
||||
|
||||
@property
|
||||
@ property
|
||||
def shelf_type(self):
|
||||
return self.shelfmember.parent.shelf_type if self.shelfmember else None
|
||||
|
||||
@property
|
||||
@ property
|
||||
def shelf_label(self):
|
||||
return self.shelfmember.parent.shelf_label if self.shelfmember else None
|
||||
|
||||
@property
|
||||
@ property
|
||||
def created_time(self):
|
||||
return self.shelfmember.created_time if self.shelfmember else None
|
||||
|
||||
@property
|
||||
@ property
|
||||
def metadata(self):
|
||||
return self.shelfmember.metadata if self.shelfmember else None
|
||||
|
||||
@property
|
||||
@ property
|
||||
def visibility(self):
|
||||
return self.shelfmember.visibility if self.shelfmember else None
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def tags(self):
|
||||
return self.owner.tag_manager.get_item_tags(self.item)
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def rating(self):
|
||||
return Rating.get_item_rating_by_user(self.item, self.owner)
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def comment(self):
|
||||
return Comment.objects.filter(owner=self.owner, item=self.item).first()
|
||||
|
||||
@property
|
||||
@ property
|
||||
def text(self):
|
||||
return self.comment.text if self.comment else None
|
||||
|
||||
@cached_property
|
||||
@ cached_property
|
||||
def review(self):
|
||||
return Review.objects.filter(owner=self.owner, item=self.item).first()
|
||||
|
||||
|
|
96
journal/templates/review.html
Normal file
96
journal/templates/review.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load thumb %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title" content="{{ site_name }}书评 - {{ review.title }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:article:author" content="{{ review.owner.username }}">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{{ review.item.cover|thumb:'normal' }}">
|
||||
<title>{{ site_name }}{% trans '评论' %} - {{ review.title }}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content">
|
||||
<div class="grid">
|
||||
<div class="grid__main" id="main">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="review-head">
|
||||
<h5 class="review-head__title">
|
||||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.visibility > 0 %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<div class="review-head__body">
|
||||
<div class="review-head__info">
|
||||
|
||||
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
|
||||
|
||||
{% if mark %}
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:0 }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<span class="review-head__time">{{ review.edited_time }}</span>
|
||||
|
||||
</div>
|
||||
<div class="review-head__actions">
|
||||
{% if request.user == review.owner %}
|
||||
<a class="review-head__action-link" href="{% url 'journal:update_review' review.id %}">{% trans '编辑' %}</a>
|
||||
<a class="review-head__action-link" href="{% url 'journal:delete_review' review.id %}">{% trans '删除' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="dividing-line"></div> -->
|
||||
<div id="rawContent">
|
||||
{{ review.html_content | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grid__aside" id="aside">
|
||||
{% include "sidebar_item.html" with item=review.item %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$(".markdownx textarea").hide();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -8,4 +8,7 @@ urlpatterns = [
|
|||
path('like/<str:piece_uuid>', like, name='like'),
|
||||
path('mark/<str:item_uuid>', mark, name='mark'),
|
||||
path('add_to_collection/<str:item_uuid>', add_to_collection, name='add_to_collection'),
|
||||
|
||||
path('review/<str:piece_uuid>', review_retrieve, name='review_retrieve'),
|
||||
path('review/create', review_create, name='review_create'),
|
||||
]
|
||||
|
|
|
@ -18,7 +18,6 @@ import time
|
|||
from management.models import Announcement
|
||||
from django.utils.baseconv import base62
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
PAGE_SIZE = 10
|
||||
|
||||
|
@ -103,3 +102,36 @@ def mark(request, item_uuid):
|
|||
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")
|
||||
if not piece.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
return render(request, 'review.html', {'review': piece})
|
||||
|
||||
|
||||
def review_edit(request, piece_uuid):
|
||||
pass
|
||||
|
||||
|
||||
def review_create(request):
|
||||
pass
|
||||
|
||||
|
||||
def mark_list(request, shelf_type, item_category):
|
||||
pass
|
||||
|
||||
|
||||
def review_list(request):
|
||||
pass
|
||||
|
||||
|
||||
def collection_list(request):
|
||||
pass
|
||||
|
||||
|
||||
def liked_list(request):
|
||||
pass
|
||||
|
|
|
@ -25,3 +25,4 @@ dnspython
|
|||
typesense
|
||||
markdownify
|
||||
igdb-api-v4
|
||||
mistune
|
||||
|
|
Loading…
Add table
Reference in a new issue