Merge pull request #43 from doubaniux/tag-feature
add tag feature | close #19
This commit is contained in:
commit
1e93379e0e
25 changed files with 586 additions and 86 deletions
|
@ -86,7 +86,7 @@ if DEBUG:
|
|||
'NAME': 'test',
|
||||
'USER': 'donotban',
|
||||
'PASSWORD': 'donotbansilvousplait',
|
||||
'HOST': '172.17.132.10',
|
||||
'HOST': '172.17.132.12',
|
||||
'OPTIONS': {
|
||||
'client_encoding': 'UTF8',
|
||||
# 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
|
||||
|
|
|
@ -4,4 +4,5 @@ from .models import *
|
|||
admin.site.register(Book)
|
||||
admin.site.register(BookMark)
|
||||
admin.site.register(BookReview)
|
||||
admin.site.register(BookTag)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.contrib.postgres.forms import SimpleArrayField
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import Book, BookMark, BookReview
|
||||
from common.models import MarkStatusEnum
|
||||
from common.forms import RadioBooleanField, RatingValidator
|
||||
from common.forms import RadioBooleanField, RatingValidator, TagField, TagInput
|
||||
from common.forms import PreviewImageInput
|
||||
|
||||
|
||||
|
@ -77,6 +77,7 @@ class BookForm(forms.ModelForm):
|
|||
isbn = isbn.strip()
|
||||
return isbn
|
||||
|
||||
|
||||
class BookMarkForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
|
@ -96,6 +97,11 @@ class BookMarkForm(forms.ModelForm):
|
|||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
tags = TagField(
|
||||
required=False,
|
||||
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
|
||||
label = _("标签")
|
||||
)
|
||||
class Meta:
|
||||
model = BookMark
|
||||
fields = [
|
||||
|
|
|
@ -3,7 +3,7 @@ import django.contrib.postgres.fields as postgres
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from common.models import Resource, Mark, Review
|
||||
from common.models import Resource, Mark, Review, Tag
|
||||
from boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE
|
||||
from django.utils import timezone
|
||||
|
||||
|
@ -67,8 +67,13 @@ class Book(Resource):
|
|||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_tags_manager(self):
|
||||
return self.book_tags
|
||||
|
||||
|
||||
class BookMark(Mark):
|
||||
# maybe this is the better solution, for it has less complex index
|
||||
# book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_marks', null=True, unique=True)
|
||||
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_marks', null=True)
|
||||
class Meta:
|
||||
constraints = [
|
||||
|
@ -82,3 +87,12 @@ class BookReview(Review):
|
|||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner', 'book'], name="unique_book_review")
|
||||
]
|
||||
|
||||
|
||||
class BookTag(Tag):
|
||||
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_tags', null=True)
|
||||
mark = models.ForeignKey(BookMark, on_delete=models.CASCADE, related_name='mark_tags', null=True)
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['content', 'mark'], name="unique_mark_tag")
|
||||
]
|
|
@ -102,6 +102,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tag-collection">
|
||||
|
||||
{% for tag_dict in book_tag_list %}
|
||||
{% for k, v in tag_dict.items %}
|
||||
{% if k == 'content' %}
|
||||
<span class="tag-collection__tag">
|
||||
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
|
@ -212,6 +225,13 @@
|
|||
{% if mark.text %}
|
||||
<p class="mark-panel__text">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
<div class="tag-collection">
|
||||
|
||||
{% for tag in mark_tags %}
|
||||
<span class="tag-collection__tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="action-panel" id="addMarkPanel">
|
||||
|
@ -264,11 +284,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="modals">
|
||||
<div class="mark-modal modal">
|
||||
<div class="mark-modal__head">
|
||||
|
@ -283,19 +303,20 @@
|
|||
{% else %}
|
||||
<span class="mark-modal__title">{% trans '我的标记' %}</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<span class="mark-modal__close-button modal-close">
|
||||
<span class="icon-cross">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<polygon
|
||||
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
|
||||
</polygon>
|
||||
</svg>
|
||||
</span>
|
||||
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
|
||||
</polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mark-modal__body">
|
||||
</span>
|
||||
</div>
|
||||
<div class="mark-modal__body">
|
||||
<form action="{% url 'books:create_update_mark' %}" method="post">
|
||||
{{ mark_form.media }}
|
||||
{% csrf_token %}
|
||||
{{ mark_form.id }}
|
||||
{{ mark_form.book }}
|
||||
|
@ -312,6 +333,11 @@
|
|||
|
||||
{{ mark_form.text }}
|
||||
|
||||
<div class="mark-modal__tag">
|
||||
<label>{{ mark_form.tags.label }}</label>
|
||||
{{ mark_form.tags }}
|
||||
</div>
|
||||
|
||||
<div class="mark-modal__option">
|
||||
<div class="mark-modal__visibility-radio">
|
||||
<span>{{ mark_form.is_private.label }}:</span>
|
||||
|
|
|
@ -95,28 +95,41 @@
|
|||
<p class="entity-list__entity-brief">
|
||||
{{ mark.book.brief | truncate:170 }}
|
||||
</p>
|
||||
<div class="entity-marks" style="margin-bottom: 0;">
|
||||
<ul class="entity-marks__mark-list">
|
||||
<li class="entity-marks__mark">
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in mark.book.tag_list %}
|
||||
{% for k, v in tag_dict.items %}
|
||||
{% if k == 'content' %}
|
||||
<span class="tag-collection__tag">
|
||||
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="dividing-line dividing-line--dashed"></div>
|
||||
<div class="entity-marks" style="margin-bottom: 0;">
|
||||
<ul class="entity-marks__mark-list">
|
||||
<li class="entity-marks__mark">
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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-marks__mark-time">{{ mark.edited_time }}</span>
|
||||
{% if mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
|
||||
{% if mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
|
107
books/views.py
107
books/views.py
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.http import HttpResponseBadRequest, HttpResponseServerError
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
@ -25,6 +26,8 @@ MARK_PER_PAGE = 20
|
|||
REVIEW_NUMBER = 5
|
||||
# how many reviews at the mark page
|
||||
REVIEW_PER_PAGE = 20
|
||||
# max tags on detail page
|
||||
TAG_NUMBER = 10
|
||||
|
||||
|
||||
# public data
|
||||
|
@ -89,18 +92,18 @@ def update(request, id):
|
|||
form.save()
|
||||
else:
|
||||
return render(
|
||||
request,
|
||||
'books/create_update.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _('修改书籍'),
|
||||
'submit_url': reverse("books:update", args=[book.id])
|
||||
}
|
||||
)
|
||||
request,
|
||||
'books/create_update.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _('修改书籍'),
|
||||
'submit_url': reverse("books:update", args=[book.id])
|
||||
}
|
||||
)
|
||||
return redirect(reverse("books:retrieve", args=[form.instance.id]))
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
|
@ -109,43 +112,58 @@ def retrieve(request, id):
|
|||
if request.method == 'GET':
|
||||
book = get_object_or_404(Book, pk=id)
|
||||
mark = None
|
||||
mark_tags = None
|
||||
review = None
|
||||
|
||||
# retreive tags
|
||||
book_tag_list = book.book_tags.values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER]
|
||||
|
||||
# retrieve user mark and initialize mark form
|
||||
try:
|
||||
if request.user.is_authenticated:
|
||||
mark = BookMark.objects.get(owner=request.user, book=book)
|
||||
except ObjectDoesNotExist:
|
||||
mark = None
|
||||
if mark:
|
||||
mark_tags = mark.mark_tags.all()
|
||||
mark.get_status_display = BookMarkStatusTranslator(mark.status)
|
||||
mark_form = BookMarkForm(instance=mark)
|
||||
mark_form = BookMarkForm(instance=mark, initial={
|
||||
'tags': mark_tags
|
||||
})
|
||||
else:
|
||||
mark_form = BookMarkForm(initial={
|
||||
'book': book
|
||||
'book': book,
|
||||
'tags': mark_tags
|
||||
})
|
||||
|
||||
# retrieve user review
|
||||
try:
|
||||
if request.user.is_authenticated:
|
||||
review = BookReview.objects.get(owner=request.user, book=book)
|
||||
except ObjectDoesNotExist:
|
||||
review = None
|
||||
|
||||
|
||||
# retrieve other related reviews and marks
|
||||
if request.user.is_anonymous:
|
||||
# hide all marks and reviews for anonymous user
|
||||
mark_list = None
|
||||
review_list = None
|
||||
mark_list_more = None
|
||||
review_list_more = None
|
||||
else:
|
||||
mark_list = BookMark.get_available(book, request.user, request.session['oauth_token'])
|
||||
review_list = BookReview.get_available(book, request.user, request.session['oauth_token'])
|
||||
mark_list = BookMark.get_available(
|
||||
book, request.user, request.session['oauth_token'])
|
||||
review_list = BookReview.get_available(
|
||||
book, request.user, request.session['oauth_token'])
|
||||
mark_list_more = True if len(mark_list) > MARK_NUMBER else False
|
||||
mark_list = mark_list[:MARK_NUMBER]
|
||||
for m in mark_list:
|
||||
m.get_status_display = BookMarkStatusTranslator(m.status)
|
||||
review_list_more = True if len(review_list) > REVIEW_NUMBER else False
|
||||
review_list_more = True if len(
|
||||
review_list) > REVIEW_NUMBER else False
|
||||
review_list = review_list[:REVIEW_NUMBER]
|
||||
|
||||
|
||||
# def strip_html_tags(text):
|
||||
# import re
|
||||
# regex = re.compile('<.*?>')
|
||||
|
@ -167,6 +185,8 @@ def retrieve(request, id):
|
|||
'mark_list_more': mark_list_more,
|
||||
'review_list': review_list,
|
||||
'review_list_more': review_list_more,
|
||||
'book_tag_list': book_tag_list,
|
||||
'mark_tags': mark_tags,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
@ -193,7 +213,7 @@ def delete(request, id):
|
|||
else:
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
# user owned entites
|
||||
|
@ -208,9 +228,11 @@ def create_update_mark(request):
|
|||
if request.method == 'POST':
|
||||
pk = request.POST.get('id')
|
||||
old_rating = None
|
||||
old_tags = None
|
||||
if pk:
|
||||
mark = get_object_or_404(BookMark, pk=pk)
|
||||
old_rating = mark.rating
|
||||
old_tags = mark.mark_tags.all()
|
||||
# update
|
||||
form = BookMarkForm(request.POST, instance=mark)
|
||||
else:
|
||||
|
@ -224,11 +246,23 @@ def create_update_mark(request):
|
|||
form.instance.owner = request.user
|
||||
form.instance.edited_time = timezone.now()
|
||||
book = form.instance.book
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# update book rating
|
||||
book.update_rating(old_rating, form.instance.rating)
|
||||
form.save()
|
||||
# update tags
|
||||
if old_tags:
|
||||
for tag in old_tags:
|
||||
tag.delete()
|
||||
if form.cleaned_data['tags']:
|
||||
for tag in form.cleaned_data['tags']:
|
||||
BookTag.objects.create(
|
||||
content=tag,
|
||||
book=book,
|
||||
mark=form.instance
|
||||
)
|
||||
except IntegrityError as e:
|
||||
return HttpResponseServerError()
|
||||
|
||||
|
@ -237,9 +271,11 @@ def create_update_mark(request):
|
|||
visibility = TootVisibilityEnum.PRIVATE
|
||||
else:
|
||||
visibility = TootVisibilityEnum.UNLISTED
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve", args=[book.id])
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve",
|
||||
args=[book.id])
|
||||
words = BookMarkStatusTranslator(int(form.cleaned_data['status'])) +\
|
||||
f"《{book.title}》" + rating_to_emoji(form.cleaned_data['rating'])
|
||||
f"《{book.title}》" + \
|
||||
rating_to_emoji(form.cleaned_data['rating'])
|
||||
content = words + '\n' + url + '\n' + form.cleaned_data['text']
|
||||
post_toot(content, visibility, request.session['oauth_token'])
|
||||
else:
|
||||
|
@ -255,11 +291,13 @@ def create_update_mark(request):
|
|||
def retrieve_mark_list(request, book_id):
|
||||
if request.method == 'GET':
|
||||
book = get_object_or_404(Book, pk=book_id)
|
||||
queryset = BookMark.get_available(book, request.user, request.session['oauth_token'])
|
||||
queryset = BookMark.get_available(
|
||||
book, request.user, request.session['oauth_token'])
|
||||
paginator = Paginator(queryset, MARK_PER_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)
|
||||
marks.pagination = PageLinksGenerator(
|
||||
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
for m in marks:
|
||||
m.get_status_display = BookMarkStatusTranslator(m.status)
|
||||
return render(
|
||||
|
@ -317,9 +355,11 @@ def create_review(request, book_id):
|
|||
visibility = TootVisibilityEnum.PRIVATE
|
||||
else:
|
||||
visibility = TootVisibilityEnum.UNLISTED
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve_review", args=[form.instance.id])
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve_review",
|
||||
args=[form.instance.id])
|
||||
words = "发布了关于" + f"《{form.instance.book.title}》" + "的评论"
|
||||
content = words + '\n' + url + '\n' + form.cleaned_data['title']
|
||||
content = words + '\n' + url + \
|
||||
'\n' + form.cleaned_data['title']
|
||||
post_toot(content, visibility, request.session['oauth_token'])
|
||||
return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
|
||||
else:
|
||||
|
@ -336,7 +376,7 @@ def update_review(request, id):
|
|||
if request.method == 'GET':
|
||||
review = get_object_or_404(BookReview, pk=id)
|
||||
if request.user != review.owner:
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponseBadRequest()
|
||||
form = BookReviewForm(instance=review)
|
||||
book = review.book
|
||||
return render(
|
||||
|
@ -362,15 +402,17 @@ def update_review(request, id):
|
|||
visibility = TootVisibilityEnum.PRIVATE
|
||||
else:
|
||||
visibility = TootVisibilityEnum.UNLISTED
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve_review", args=[form.instance.id])
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve_review",
|
||||
args=[form.instance.id])
|
||||
words = "发布了关于" + f"《{form.instance.book.title}》" + "的评论"
|
||||
content = words + '\n' + url + '\n' + form.cleaned_data['title']
|
||||
content = words + '\n' + url + \
|
||||
'\n' + form.cleaned_data['title']
|
||||
post_toot(content, visibility, request.session['oauth_token'])
|
||||
return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -439,11 +481,13 @@ def retrieve_review(request, id):
|
|||
def retrieve_review_list(request, book_id):
|
||||
if request.method == 'GET':
|
||||
book = get_object_or_404(Book, pk=book_id)
|
||||
queryset = BookReview.get_available(book, request.user, request.session['oauth_token'])
|
||||
queryset = BookReview.get_available(
|
||||
book, request.user, request.session['oauth_token'])
|
||||
paginator = Paginator(queryset, REVIEW_PER_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)
|
||||
reviews.pagination = PageLinksGenerator(
|
||||
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
return render(
|
||||
request,
|
||||
'books/review_list.html',
|
||||
|
@ -485,7 +529,8 @@ def click_to_scrape(request):
|
|||
return render(request, 'common/error.html', {'msg': _("爬取数据失败😫,请重试")})
|
||||
except ValueError:
|
||||
return render(request, 'common/error.html', {'msg': _("链接非法,爬取失败")})
|
||||
scraped_cover = {'cover': SimpleUploadedFile('temp.jpg', raw_cover)}
|
||||
scraped_cover = {
|
||||
'cover': SimpleUploadedFile('temp.jpg', raw_cover)}
|
||||
form = BookForm(scraped_book, scraped_cover)
|
||||
if form.is_valid():
|
||||
form.instance.last_editor = request.user
|
||||
|
@ -500,4 +545,4 @@ def click_to_scrape(request):
|
|||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponseBadRequest()
|
||||
|
|
|
@ -6,7 +6,6 @@ import json
|
|||
|
||||
|
||||
class KeyValueInput(forms.Widget):
|
||||
""" jQeury required """
|
||||
template_name = 'widgets/key_value.html'
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
|
@ -75,4 +74,37 @@ class PreviewImageInput(forms.FileInput):
|
|||
"""
|
||||
Return whether value is considered to be initial value.
|
||||
"""
|
||||
return bool(value and getattr(value, 'url', False))
|
||||
return bool(value and getattr(value, 'url', False))
|
||||
|
||||
|
||||
class TagInput(forms.TextInput):
|
||||
"""
|
||||
Dump tag queryset into tag list
|
||||
"""
|
||||
template_name = 'widgets/tag.html'
|
||||
def format_value(self, value):
|
||||
if value == '' or value is None or len(value) == 0:
|
||||
return ''
|
||||
tag_list = []
|
||||
try:
|
||||
tag_list = [t['content'] for t in value]
|
||||
except TypeError:
|
||||
tag_list = [t.content for t in value]
|
||||
# return ','.join(tag_list)
|
||||
return tag_list
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('lib/css/tag-input.css',)
|
||||
}
|
||||
js = ('lib/js/tag-input.js',)
|
||||
|
||||
|
||||
class TagField(forms.CharField):
|
||||
"""
|
||||
Split comma connected string into tag list
|
||||
"""
|
||||
widget = TagInput
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
return [t.strip() for t in value.split(',')]
|
||||
|
|
|
@ -76,6 +76,13 @@ class Resource(models.Model):
|
|||
self.calculate_rating(old_rating, new_rating)
|
||||
self.save()
|
||||
|
||||
def get_tags_manager(self):
|
||||
"""
|
||||
Since relation between tag and resource is foreign key, and related name has to be unique,
|
||||
this method works like interface.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class UserOwnedEntity(models.Model):
|
||||
is_private = models.BooleanField()
|
||||
|
@ -142,6 +149,9 @@ class Mark(UserOwnedEntity):
|
|||
rating = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
text = models.CharField(max_length=500, blank=True, default='')
|
||||
|
||||
def __str__(self):
|
||||
return f"({self.id}) {self.owner} {self.status}"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
constraints = [
|
||||
|
@ -159,3 +169,13 @@ class Review(UserOwnedEntity):
|
|||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
content = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.content
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
|
|
@ -929,7 +929,7 @@ select::placeholder {
|
|||
}
|
||||
|
||||
.mark-modal .mark-modal__head {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mark-modal .mark-modal__head::after {
|
||||
|
@ -946,6 +946,7 @@ select::placeholder {
|
|||
|
||||
.mark-modal .mark-modal__close-button {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mark-modal .mark-modal__confirm-button {
|
||||
|
@ -994,6 +995,10 @@ select::placeholder {
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
.mark-modal .mark-modal__tag {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mark-modal .mark-modal__option {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
@ -1045,7 +1050,7 @@ select::placeholder {
|
|||
}
|
||||
|
||||
.confirm-modal .confirm-modal__head {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.confirm-modal .confirm-modal__head::after {
|
||||
|
@ -1062,6 +1067,7 @@ select::placeholder {
|
|||
|
||||
.confirm-modal .confirm-modal__close-button {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.confirm-modal .confirm-modal__confirm-button {
|
||||
|
@ -1111,6 +1117,7 @@ select::placeholder {
|
|||
.entity-list .entity-list__entity-text {
|
||||
margin-left: 20px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entity-list .entity-list__entity-link {
|
||||
|
@ -1141,6 +1148,7 @@ select::placeholder {
|
|||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.entity-list .entity-list__rating {
|
||||
|
@ -1189,6 +1197,7 @@ select::placeholder {
|
|||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 48%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.entity-detail .entity-detail__fields div, .entity-detail .entity-detail__fields span {
|
||||
|
@ -1345,6 +1354,13 @@ select::placeholder {
|
|||
border-top: solid 1px #ccc;
|
||||
}
|
||||
|
||||
.dividing-line.dividing-line--dashed {
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 2px;
|
||||
border-top: 1px dashed #e5e5e5;
|
||||
}
|
||||
|
||||
.entity-sort {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
@ -1467,6 +1483,32 @@ select::placeholder {
|
|||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tag-collection {
|
||||
position: relative;
|
||||
left: -3px;
|
||||
}
|
||||
|
||||
.tag-collection .tag-collection__tag {
|
||||
position: relative;
|
||||
display: block;
|
||||
float: left;
|
||||
color: white;
|
||||
background: #d5d5d5;
|
||||
padding: 5px;
|
||||
border-radius: .3rem;
|
||||
line-height: 1.2em;
|
||||
font-size: 80%;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.tag-collection .tag-collection__tag a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tag-collection .tag-collection__tag a:hover {
|
||||
color: #00a1cc;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.entity-list .entity-list__entity {
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -1718,6 +1760,7 @@ select::placeholder {
|
|||
padding: 0px 3px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-relation .user-relation__related-user > a:hover {
|
||||
|
@ -1732,7 +1775,6 @@ select::placeholder {
|
|||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ $(document).ready( function() {
|
|||
|
||||
// if wish, hide rating widget in modal
|
||||
if ($(this).attr("id") == "wishButton") {
|
||||
console.log($(this).attr("id"))
|
||||
// console.log($(this).attr("id"))
|
||||
$(".mark-modal .rating-star-edit").hide();
|
||||
} else {
|
||||
$(".mark-modal .rating-star-edit").show();
|
||||
|
|
|
@ -39,7 +39,7 @@ $(document).ready( function() {
|
|||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-followers").hide();
|
||||
$(".mast-followers").before("<div>暂无</div>");
|
||||
$(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
|
||||
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
|
@ -72,7 +72,7 @@ $(document).ready( function() {
|
|||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-following").hide();
|
||||
$(".mast-following").before("<div>暂无</div>");
|
||||
$(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
userList = userList.slice(0, 4);
|
||||
|
|
65
common/static/lib/css/tag-input.css
Normal file
65
common/static/lib/css/tag-input.css
Normal file
|
@ -0,0 +1,65 @@
|
|||
@charset "UTF-8";
|
||||
.tag-input {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 0.1rem solid #ccc;
|
||||
padding-bottom: 2px;
|
||||
|
||||
}
|
||||
|
||||
.tag-input__tag {
|
||||
position: relative;
|
||||
display: block;
|
||||
float: left;
|
||||
color: #00a1cc;
|
||||
background: transparent;
|
||||
padding: 5px 20px 5px 5px;
|
||||
margin: 4px;
|
||||
border: 0.1rem solid #00a1cc;
|
||||
border-radius: .4rem;
|
||||
line-height: 1em;
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.tag-input__tag.tag-input__tag--highlight {
|
||||
color: white;
|
||||
background: #00a1cc;
|
||||
}
|
||||
|
||||
.tag-input__close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 0 2px 2px 0;
|
||||
-webkit-transition: background 0.2s;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tag-input__close:after {
|
||||
position: absolute;
|
||||
content: "×";
|
||||
top: 5px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.tag-input__close:hover {
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.tag-input input {
|
||||
border: 0;
|
||||
margin: 4px;
|
||||
padding: 3px 7px 3px 0;
|
||||
width: auto;
|
||||
outline: none;
|
||||
}
|
146
common/static/lib/js/tag-input.js
Normal file
146
common/static/lib/js/tag-input.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
function inputTags(configs) {
|
||||
|
||||
|
||||
let tagsContainer = configs.container,
|
||||
input = configs.container.querySelector('input')
|
||||
|
||||
let _privateMethods = {
|
||||
|
||||
init: function (configs) {
|
||||
|
||||
// this.inspectConfigProperties(configs);
|
||||
|
||||
let self = this,
|
||||
input_hidden = document.createElement('input');
|
||||
let name = input.getAttribute('name'),
|
||||
id = input.getAttribute('id');
|
||||
input.removeAttribute('name');
|
||||
// input.removeAttribute('id');
|
||||
input_hidden.setAttribute('type', 'hidden');
|
||||
// input_hidden.setAttribute('id', id);
|
||||
input_hidden.setAttribute('name', name);
|
||||
input.parentNode.insertBefore(input_hidden, input);
|
||||
this.input_hidden = input_hidden
|
||||
|
||||
tagsContainer.addEventListener('click', function () {
|
||||
input.focus();
|
||||
});
|
||||
|
||||
if (configs.tags) {
|
||||
for (let i = 0; i < configs.tags.length; i++) {
|
||||
if (configs.tags[i]) {
|
||||
this.create(configs.tags[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener("focusout", function () {
|
||||
|
||||
let tag_txt = this.value.trim(),
|
||||
tag_exists = false;
|
||||
|
||||
if (self.tags_array) {
|
||||
tag_exists = Boolean(self.tags_array.indexOf(tag_txt) + 1);
|
||||
}
|
||||
|
||||
if (tag_txt && tag_exists && !configs.allowDuplicateTags) {
|
||||
self.showDuplicate(tag_txt);
|
||||
}
|
||||
else if (tag_txt && tag_exists && configs.allowDuplicateTags) {
|
||||
self.create(tag_txt);
|
||||
}
|
||||
else if (tag_txt && !tag_exists) {
|
||||
self.create(tag_txt);
|
||||
}
|
||||
this.value = "";
|
||||
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function (ev) {
|
||||
|
||||
|
||||
if (ev.keyCode === 13 || ev.keyCode === 188 ||
|
||||
(ev.keyCode === 32 && configs.allowDuplicateTags)) { // enter || comma || space
|
||||
let event = new Event('focusout');
|
||||
input.dispatchEvent(event);
|
||||
ev.preventDefault();
|
||||
}
|
||||
else if (event.which === 8 && !input.value) { // backspace
|
||||
let tag_nodes = document.querySelectorAll('.tag-input__tag');
|
||||
if (tag_nodes.length > 0) {
|
||||
input.addEventListener('keyup', function (event) {
|
||||
if (event.which === 8) {
|
||||
let node_to_del = tag_nodes[tag_nodes.length - 1];
|
||||
node_to_del.remove();
|
||||
self.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
create: function (tag_txt) {
|
||||
|
||||
let tag_nodes = document.querySelectorAll('.tag-input__tag');
|
||||
|
||||
if (!configs.maxTags || tag_nodes.length < configs.maxTags) {
|
||||
let self = this,
|
||||
span_tag = document.createElement('span'),
|
||||
input_hidden_field = self.input_hidden;
|
||||
|
||||
span_tag.setAttribute('class', 'tag-input__tag');
|
||||
span_tag.innerText = tag_txt;
|
||||
|
||||
let span_tag_close = document.createElement('span');
|
||||
span_tag_close.setAttribute('class', 'tag-input__close');
|
||||
span_tag.appendChild(span_tag_close);
|
||||
|
||||
tagsContainer.insertBefore(span_tag, input_hidden_field);
|
||||
|
||||
span_tag.childNodes[1].addEventListener('click', function () {
|
||||
self.remove(this);
|
||||
});
|
||||
|
||||
this.update();
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
update: function () {
|
||||
|
||||
let tags = document.getElementsByClassName('tag-input__tag'),
|
||||
tags_arr = [];
|
||||
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
tags_arr.push(tags[i].textContent.toLowerCase());
|
||||
}
|
||||
this.tags_array = tags_arr;
|
||||
|
||||
this.input_hidden.setAttribute('value', tags_arr.join());
|
||||
},
|
||||
|
||||
remove: function (tag) {
|
||||
configs.onTagRemove(tag.parentNode.textContent);
|
||||
tag.parentNode.remove();
|
||||
this.update();
|
||||
},
|
||||
|
||||
showDuplicate: function (tag_value) {
|
||||
let tags = document.getElementsByClassName('tag-input__tag');
|
||||
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
if (tags[i].textContent === tag_value) {
|
||||
tags[i].classList.add("tag-input__tag--highlight");
|
||||
window.setTimeout(function () {
|
||||
tags[i].classList.remove("tag-input__tag--highlight");
|
||||
}, configs.duplicateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_privateMethods.init(configs);
|
||||
// return false;
|
||||
}
|
|
@ -127,6 +127,7 @@ $aside-section-padding-mobile: 24px 25px 10px 25px
|
|||
padding: 0px 3px
|
||||
text-align: center
|
||||
display: inline-block
|
||||
overflow: hidden
|
||||
& > a
|
||||
&:hover
|
||||
color: $color-secondary
|
||||
|
@ -138,7 +139,7 @@ $aside-section-padding-mobile: 24px 25px 10px 25px
|
|||
color: inherit
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
display: -webkit-box;
|
||||
// display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ $sub-section-title-margin: 8px
|
|||
& &__entity-text
|
||||
margin-left: 20px
|
||||
overflow: hidden
|
||||
width: 100%
|
||||
// float: left
|
||||
|
||||
& &__entity-link
|
||||
|
@ -65,6 +66,7 @@ $sub-section-title-margin: 8px
|
|||
-webkit-box-orient: vertical
|
||||
-webkit-line-clamp: 4
|
||||
overflow: hidden
|
||||
margin-bottom: 0
|
||||
|
||||
$rating-info-gap-width: 5px
|
||||
& &__rating
|
||||
|
@ -109,6 +111,7 @@ $sub-section-title-margin: 8px
|
|||
display: inline-block
|
||||
vertical-align: top
|
||||
width: 48%
|
||||
margin-bottom: 5px
|
||||
& div, & span
|
||||
margin: 1px 0
|
||||
|
||||
|
@ -233,6 +236,11 @@ $mark-review-padding-wider: 6px 0
|
|||
width: 100%
|
||||
margin: 40px 0 24px 0
|
||||
border-top: solid 1px $color-light
|
||||
&.dividing-line--dashed
|
||||
margin: 0
|
||||
margin-top: 10px
|
||||
margin-bottom: 2px
|
||||
border-top: 1px dashed $color-quinary;
|
||||
|
||||
// on home page
|
||||
.entity-sort
|
||||
|
@ -328,6 +336,29 @@ $mark-review-padding-wider: 6px 0
|
|||
& &__action-link
|
||||
&:not(:first-child)
|
||||
margin-left: 5px
|
||||
|
||||
// tag list
|
||||
.tag-collection
|
||||
$tag-margin: 3px
|
||||
position: relative
|
||||
left: -$tag-margin
|
||||
& &__tag
|
||||
position: relative;
|
||||
display: block;
|
||||
float: left;
|
||||
color: white;
|
||||
background: $color-quaternary;
|
||||
padding: 5px;
|
||||
// margin: 4px;
|
||||
border-radius: .3rem;
|
||||
line-height: 1.2em;
|
||||
font-size: 80%
|
||||
margin: 3px
|
||||
& a
|
||||
color: white
|
||||
&:hover
|
||||
color: $color-primary
|
||||
|
||||
// Small devices (landscape phones, 576px and up)
|
||||
@media (max-width: $small-devices)
|
||||
.entity-list
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
padding: 20px 20px 10px 20px
|
||||
color: $color-secondary
|
||||
& &__head
|
||||
margin-bottom: 30px
|
||||
margin-bottom: 20px
|
||||
&::after
|
||||
@include clear
|
||||
|
||||
|
@ -32,6 +32,7 @@
|
|||
|
||||
& &__close-button
|
||||
float: right
|
||||
cursor: pointer
|
||||
|
||||
& &__body
|
||||
|
||||
|
@ -69,6 +70,10 @@
|
|||
margin-bottom: 5px
|
||||
resize: vertical
|
||||
|
||||
& &__tag
|
||||
// margin-top: 10px
|
||||
margin-bottom: 20px
|
||||
|
||||
& &__option
|
||||
margin-bottom: 24px
|
||||
&::after
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
min-height: 100px
|
||||
|
||||
.rating-star .jq-star
|
||||
cursor: unset !important
|
||||
cursor: unset !important
|
|
@ -33,8 +33,13 @@
|
|||
|
||||
<div class="entity-list">
|
||||
{% if request.GET.q %}
|
||||
<h5 class="entity-list__title">“{{ request.GET.q }}”{% trans '的搜索结果' %}</h5>
|
||||
<h5 class="entity-list__title">“{{ request.GET.q }}” {% trans '的搜索结果' %}</h5>
|
||||
{% endif %}
|
||||
|
||||
{% if request.GET.tag %}
|
||||
<h5 class="entity-list__title">{% trans '含有标签' %} “{{ request.GET.tag }}” {% trans '的结果' %}</h5>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-list__entities">
|
||||
|
||||
{% for book in items %}
|
||||
|
@ -95,6 +100,18 @@
|
|||
<p class="entity-list__entity-brief">
|
||||
{{ book.brief }}
|
||||
</p>
|
||||
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in book.tag_list %}
|
||||
{% for k, v in tag_dict.items %}
|
||||
{% if k == 'content' %}
|
||||
<span class="tag-collection__tag">
|
||||
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<img src="{% static 'img/logo.svg' %}" alt="" class="navbar__logo-img">
|
||||
</a>
|
||||
<form action="#" οnsubmit="" class="navbar__search-box">
|
||||
<!-- <input type="search" class="" name="q" id="searchInput" required="true" value="{% for v in request.GET.values %}{{ v }}{% endfor %}" -->
|
||||
<input type="search" class="" name="q" id="searchInput" required="true" value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}"
|
||||
placeholder="搜索书影音,多个关键字以空格分割">
|
||||
</form>
|
||||
|
|
17
common/templates/widgets/tag.html
Normal file
17
common/templates/widgets/tag.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="tag-input">
|
||||
<input type="text" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %}>
|
||||
</div>
|
||||
<script>
|
||||
new inputTags({
|
||||
container: document.getElementsByClassName("tag-input")[0],
|
||||
tags : [{% for tag in widget.value %}"{{ tag }}",{% endfor %}],
|
||||
allowDuplicateTags : false,
|
||||
duplicateTime: 300,
|
||||
onTagRemove : function (tag) {},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ from common.models import MarkStatusEnum
|
|||
from common.utils import PageLinksGenerator
|
||||
from users.models import Report, User
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Count
|
||||
from django.http import HttpResponseBadRequest
|
||||
|
||||
|
||||
|
@ -18,6 +18,9 @@ ITEMS_PER_PAGE = 20
|
|||
# how many pages links in the pagination
|
||||
PAGE_LINK_NUMBER = 7
|
||||
|
||||
# max tags on list page
|
||||
TAG_NUMBER_ON_LIST = 5
|
||||
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
|
@ -58,19 +61,30 @@ def search(request):
|
|||
# in the future when more modules are added...
|
||||
# category = request.GET.get("category")
|
||||
q = Q()
|
||||
keywords = request.GET.get("q", default='').split()
|
||||
query_args = []
|
||||
|
||||
# keywords
|
||||
keywords = request.GET.get("q", default='').split()
|
||||
for keyword in keywords:
|
||||
q = q | Q(title__icontains=keyword)
|
||||
q = q | Q(subtitle__istartswith=keyword)
|
||||
q = q | Q(orig_title__icontains=keyword)
|
||||
|
||||
# tag
|
||||
tag = request.GET.get("tag", default='')
|
||||
if tag:
|
||||
q = q | Q(book_tags__content__iexact=tag)
|
||||
|
||||
query_args.append(q)
|
||||
queryset = Book.objects.filter(*query_args)
|
||||
queryset = Book.objects.filter(*query_args).distinct()
|
||||
|
||||
paginator = Paginator(queryset, ITEMS_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
items = paginator.get_page(page_number)
|
||||
items.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
for item in items:
|
||||
item.tag_list = item.get_tags_manager().values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -81,4 +95,4 @@ def search(request):
|
|||
)
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
return HttpResponseBadRequest()
|
||||
|
|
|
@ -47,7 +47,7 @@ $(document).ready( function() {
|
|||
let subUserList = null;
|
||||
if (userList.length == 0) {
|
||||
$(".mast-followers").hide();
|
||||
$(".mast-followers").before("<div>暂无</div>");
|
||||
$(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
subUserList = userList.slice(0, 4);
|
||||
|
@ -103,7 +103,7 @@ $(document).ready( function() {
|
|||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-following").hide();
|
||||
$(".mast-following").before("<div>暂无</div>");
|
||||
$(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
userList = userList.slice(0, 4);
|
||||
|
|
|
@ -46,7 +46,7 @@ $(document).ready( function() {
|
|||
function (userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-followers").hide();
|
||||
$(".mast-followers").before("<div>暂无</div>");
|
||||
$(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
|
||||
} else {
|
||||
if (userList.length > 4) {
|
||||
userList = userList.slice(0, 4);
|
||||
|
@ -80,7 +80,7 @@ $(document).ready( function() {
|
|||
let subUserList = null;
|
||||
if (userList.length == 0) {
|
||||
$(".mast-following").hide();
|
||||
$(".mast-following").before("<div>暂无</div>");
|
||||
$(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
subUserList = userList.slice(0, 4);
|
||||
|
|
|
@ -6,12 +6,13 @@ from django.contrib.auth import authenticate
|
|||
from django.core.paginator import Paginator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Count
|
||||
from .models import User, Report
|
||||
from .forms import ReportForm
|
||||
from common.mastodon.auth import *
|
||||
from common.mastodon.api import *
|
||||
from common.mastodon import mastodon_request_included
|
||||
from common.views import BOOKS_PER_SET, ITEMS_PER_PAGE, PAGE_LINK_NUMBER
|
||||
from common.views import BOOKS_PER_SET, ITEMS_PER_PAGE, PAGE_LINK_NUMBER, TAG_NUMBER_ON_LIST
|
||||
from common.models import MarkStatusEnum
|
||||
from common.utils import PageLinksGenerator
|
||||
from books.models import *
|
||||
|
@ -307,6 +308,9 @@ def book_list(request, id, status):
|
|||
paginator = Paginator(queryset, ITEMS_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
marks = paginator.get_page(page_number)
|
||||
for mark in marks:
|
||||
mark.book.tag_list = mark.book.get_tags_manager().values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
|
||||
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书"))
|
||||
return render(
|
||||
|
@ -386,4 +390,4 @@ def auth_login(request, user, token):
|
|||
def auth_logout(request):
|
||||
""" Decorates django ``logout()``. Release token in session."""
|
||||
del request.session['oauth_token']
|
||||
auth.logout(request)
|
||||
auth.logout(request)
|
||||
|
|
Loading…
Add table
Reference in a new issue