new data model: mark and review

This commit is contained in:
Your Name 2022-12-24 01:28:24 -05:00
parent 744413b2fc
commit 116ca00a7d
13 changed files with 595 additions and 76 deletions

View file

@ -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):

View file

@ -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>

View 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">&laquo;</a>
<a href="?page={{ marks.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</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">&rsaquo;</a>
<a href="?page={{ marks.pagination.last_page }}"
class="pagination__nav-link">&raquo;</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>

View 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">&laquo;</a>
<a href="?page={{ reviews.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</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">&rsaquo;</a>
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">&raquo;</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>

View file

@ -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>

View 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>

View file

@ -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),
]

View file

@ -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,
}
)

View file

@ -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()

View 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>

View file

@ -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'),
]

View file

@ -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

View file

@ -25,3 +25,4 @@ dnspython
typesense
markdownify
igdb-api-v4
mistune