second commit
This commit is contained in:
parent
d219921bb0
commit
8ddbb9d257
52 changed files with 3889 additions and 306 deletions
|
@ -86,7 +86,7 @@ if DEBUG:
|
|||
'NAME': 'test',
|
||||
'USER': 'donotban',
|
||||
'PASSWORD': 'donotbansilvousplait',
|
||||
'HOST': '192.168.136.5',
|
||||
'HOST': '192.168.144.2',
|
||||
'OPTIONS': {
|
||||
'client_encoding': 'UTF8',
|
||||
# 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
|
||||
|
@ -112,7 +112,7 @@ else:
|
|||
# https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#authentication-backends
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'users.auth.OAuth2Backend',
|
||||
'common.mastodon.auth.OAuth2Backend',
|
||||
# for admin to login admin site
|
||||
# 'django.contrib.auth.backends.ModelBackend'
|
||||
]
|
||||
|
@ -155,6 +155,9 @@ DEFAULT_BOOK_IMAGE = os.path.join(MEDIA_ROOT, BOOK_MEDIA_PATH_ROOT, 'default.jpg
|
|||
# Mastodon domain name
|
||||
MASTODON_DOMAIN_NAME = 'cmx-im.work'
|
||||
|
||||
# Timeout of requests to Mastodon, in seconds
|
||||
MASTODON_TIMEOUT = 30
|
||||
|
||||
# Default password for each user. since assword is not used any way,
|
||||
# any string that is not empty is ok
|
||||
DEFAULT_PASSWORD = 'eBRM1DETkYgiqPgq'
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from .models import *
|
||||
|
||||
admin.site.register(Book)
|
||||
admin.site.register(BookMark)
|
||||
admin.site.register(BookReview)
|
||||
|
||||
# Register your models here.
|
||||
|
|
|
@ -3,14 +3,27 @@ from common.forms import KeyValueInput
|
|||
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
|
||||
|
||||
|
||||
def BookMarkStatusTranslator(status):
|
||||
trans_dict = {
|
||||
MarkStatusEnum.DO.value: _("在看"),
|
||||
MarkStatusEnum.WISH.value: _("想看"),
|
||||
MarkStatusEnum.COLLECT.value: _("看过")
|
||||
}
|
||||
return trans_dict[status]
|
||||
|
||||
|
||||
class BookForm(forms.ModelForm):
|
||||
pub_year = forms.IntegerField(required=False, max_value=9999, min_value=0, label=_("出版年份"))
|
||||
pub_month = forms.IntegerField(required=False, max_value=12, min_value=1, label=_("出版月份"))
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'isbn',
|
||||
'author',
|
||||
|
@ -50,27 +63,78 @@ class BookForm(forms.ModelForm):
|
|||
'author': forms.TextInput(attrs={'placeholder': _("多个作者使用英文逗号分隔")}),
|
||||
'translator': forms.TextInput(attrs={'placeholder': _("多个译者使用英文逗号分隔")}),
|
||||
'other_info': KeyValueInput(),
|
||||
# 'cover': forms.FileInput(),
|
||||
}
|
||||
|
||||
|
||||
class BookMarkForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
STATUS_CHOICES = [(v, BookMarkStatusTranslator(v)) for v in MarkStatusEnum.values]
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
share_to_mastodon = forms.BooleanField(label=_("分享到长毛象"), initial=True, required=False)
|
||||
rating = forms.IntegerField(validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
|
||||
status = forms.ChoiceField(
|
||||
label=_(""),
|
||||
widget=forms.RadioSelect(),
|
||||
choices=STATUS_CHOICES
|
||||
)
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
class Meta:
|
||||
model = BookMark
|
||||
fields = [
|
||||
'id',
|
||||
'book',
|
||||
'status',
|
||||
'rating',
|
||||
'text',
|
||||
'is_private',
|
||||
]
|
||||
labels = {
|
||||
'rating': _("评分"),
|
||||
'text': _("短评"),
|
||||
}
|
||||
widgets = {
|
||||
'book': forms.Select(attrs={"hidden": ""}),
|
||||
'text': forms.Textarea(attrs={"placeholder": _("最多只能写500字哦~")}),
|
||||
}
|
||||
|
||||
|
||||
class BookReviewForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
share_to_mastodon = forms.BooleanField(label=_("分享到长毛象"), initial=True, required=False)
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
class Meta:
|
||||
model = BookReview
|
||||
fields = [
|
||||
'id',
|
||||
'book',
|
||||
'title',
|
||||
'content',
|
||||
'is_private'
|
||||
]
|
||||
labels = {
|
||||
'book': "",
|
||||
'title': _("标题"),
|
||||
'content': _("正文"),
|
||||
'share_to_mastodon': _("分享到长毛象")
|
||||
}
|
||||
widgets = {
|
||||
'book': forms.Select(attrs={"hidden": ""}),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import uuid
|
||||
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 boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def book_cover_path(instance, filename):
|
||||
raise NotImplementedError("UUID!!!!!!!!!!!")
|
||||
ext = filename.split('.')[-1]
|
||||
filename = "%s.%s" % (uuid.uuid4(), ext)
|
||||
root = ''
|
||||
if BOOK_MEDIA_PATH_ROOT.endswith('/'):
|
||||
root = BOOK_MEDIA_PATH_ROOT
|
||||
else:
|
||||
root = BOOK_MEDIA_PATH_ROOT + '/'
|
||||
return root + datetime.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||
|
||||
|
||||
class Book(Resource):
|
||||
|
@ -44,9 +46,8 @@ class Book(Resource):
|
|||
# since data origin is not formatted and might be CNY USD or other currency, use char instead
|
||||
price = models.CharField(_("pricing"), blank=True, default='', max_length=50)
|
||||
pages = models.PositiveIntegerField(_("pages"), null=True, blank=True)
|
||||
isbn = models.CharField(_("ISBN"), blank=True, max_length=20, unique=True, db_index=True)
|
||||
isbn = models.CharField(_("ISBN"), blank=True, null=True, max_length=20, unique=True, db_index=True)
|
||||
# to store previously scrapped data
|
||||
img_url = models.URLField(max_length=300)
|
||||
cover = models.ImageField(_("cover picture"), upload_to=book_cover_path, default=DEFAULT_BOOK_IMAGE, blank=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -67,7 +68,7 @@ class Book(Resource):
|
|||
|
||||
|
||||
class BookMark(Mark):
|
||||
book = models.ForeignKey(Book, on_delete=models.SET_NULL, related_name='book_marks', null=True)
|
||||
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_marks', null=True)
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner', 'book'], name="unique_book_mark")
|
||||
|
@ -75,7 +76,7 @@ class BookMark(Mark):
|
|||
|
||||
|
||||
class BookReview(Review):
|
||||
book = models.ForeignKey(Book, on_delete=models.SET_NULL, related_name='book_reviews', null=True)
|
||||
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_reviews', null=True)
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner', 'book'], name="unique_book_review")
|
||||
|
|
7
books/static/js/scrape.js
Normal file
7
books/static/js/scrape.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
$(document).ready( function() {
|
||||
$("#submit").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#scrapeForm form").submit();
|
||||
});
|
||||
|
||||
});
|
|
@ -10,7 +10,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 添加图书' %}</title>
|
||||
<title>{% trans 'Boofilsic - ' %}{{ title }}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'js/create_update.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
|
@ -30,7 +30,9 @@
|
|||
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -38,10 +40,10 @@
|
|||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<form action="{% url 'books:create' %}" method="post">
|
||||
<form action="{{ submit_url }}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button type="submit">{% trans '提交' %}</button>
|
||||
<input class="button" type="submit" value="{% trans '提交' %}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
128
books/templates/books/create_update_review.html
Normal file
128
books/templates/books/create_update_review.html
Normal file
|
@ -0,0 +1,128 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - ' %}{{ title }}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'js/create_update_review.js' %}"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<h4 class="nav-title">{{ title }}</h4>
|
||||
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<div class="item-card clearfix">
|
||||
<img src="{{ book.cover.url }}" alt="" class="item-image float-left">
|
||||
<div class="item-info float-left">
|
||||
|
||||
<div class="item-title"><a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a></div>
|
||||
<div>{% if book.isbn %}{% trans 'ISBN:' %}{{ book.isbn }}{% endif %}</div>
|
||||
<div>{% if book.author %}{% trans '作者:' %}
|
||||
{% for author in book.author %}
|
||||
<span>{{ author }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}</div>
|
||||
<div>{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}</div>
|
||||
<div>{%if book.pub_year %}{% trans '出版时间:' %}{{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{ book.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
|
||||
|
||||
{% if book.rating %}
|
||||
<span class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"> </span>
|
||||
<span class="rating-score"> {{ book.rating }} </span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="review">
|
||||
<form action="{{ submit_url }}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.book }}
|
||||
{{ form.title.label }}{{ form.title }}
|
||||
<div class="clearfix">
|
||||
<span class="float-left">
|
||||
{{ form.content.label }}
|
||||
</span>
|
||||
<span class="float-right">
|
||||
<span class="preview-button">{% trans '预览' %}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="rawContent">
|
||||
{{ form.content }}
|
||||
</div>
|
||||
<div class="fyi">{% trans '不知道什么是Markdown?可以参考' %}<a target="_blank" href="https://www.markdownguide.org/">{% trans '这里' %}</a></div>
|
||||
<div class="option clearfix">
|
||||
<div class="selection float-left">
|
||||
|
||||
{{ form.is_private.label }}{{ form.is_private }}
|
||||
</div>
|
||||
<div class="float-right">
|
||||
{{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<input class="button float-right" type="submit" value="{% trans '提交' %}">
|
||||
</div>
|
||||
{{ form.media }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -0,0 +1,107 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 删除图书' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/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 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<h4 class="nav-title">{% trans '删除图书' %}</h4>
|
||||
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<h4 class="prompt">{% trans '确认删除这本书吗?相关评论和标记将一并删除。' %}</h4>
|
||||
|
||||
<div class="item-card">
|
||||
<div class="clearfix">
|
||||
<img src="{{ book.cover.url }}" alt="" class="item-image float-left">
|
||||
<div class="item-info float-left">
|
||||
<a href="{% url 'books:retrieve' book.id %}">
|
||||
<h5 class="item-title">
|
||||
{{ book.title }}
|
||||
</h5>
|
||||
</a>
|
||||
{% if book.rating %}
|
||||
{% trans '评分:' %}
|
||||
<span class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}">
|
||||
</span>
|
||||
<span class="rating-score">{{ book.rating }} </span>
|
||||
{% else %}
|
||||
<span>{% trans '评分:暂无评分' %}</span>
|
||||
{% endif %}
|
||||
<div>{% trans '最近编辑者:' %}{{ book.last_editor | default:"" }}</div>
|
||||
<div>{% trans '上次编辑时间:' %}{{ book.edited_time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<form action="{% url 'books:delete' book.id %}" method="post" class="float-right">
|
||||
{% csrf_token %}
|
||||
<input class="button" type="submit" value="{% trans '确认' %}">
|
||||
</form>
|
||||
<button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
109
books/templates/books/delete_review.html
Normal file
109
books/templates/books/delete_review.html
Normal file
|
@ -0,0 +1,109 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 删除评论' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<h4 class="nav-title">{% trans '删除评论' %}</h4>
|
||||
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<h4 class="prompt">{% trans '确认删除这篇评论吗?' %}</h4>
|
||||
|
||||
<div class="item-card">
|
||||
<div class="review">
|
||||
<div class="review-head clearfix">
|
||||
<div class="mark-label float-left">
|
||||
<h4>
|
||||
{{ review.title }}
|
||||
{% if review.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 %}
|
||||
</h4>
|
||||
<a href="" class="mark-owner-name">{{ review.owner.username }}</a>
|
||||
|
||||
<span class="mark-time">{{ review.edited_time }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- <div class="dividing-line"></div> -->
|
||||
<div id="rawContent" class="delete-preview">
|
||||
{{ form.content }}
|
||||
</div>
|
||||
{{ form.media }}
|
||||
</div>
|
||||
<!-- <div class="dividing-line"></div> -->
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<form action="{% url 'books:delete_review' review.id %}" method="post" class="float-right">
|
||||
{% csrf_token %}
|
||||
<input class="button" type="submit" value="{% trans '确认' %}">
|
||||
</form>
|
||||
<button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
$(".markdownx textarea").hide();
|
||||
$(".markdownx .markdownx-preview").show();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -15,6 +15,7 @@
|
|||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/detail.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_modal.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
</head>
|
||||
|
@ -31,7 +32,9 @@
|
|||
<input type="search" class="search-box" name="keywords" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -40,7 +43,9 @@
|
|||
<div class="row">
|
||||
<div id="main">
|
||||
<div class="clearfix">
|
||||
<img src="{% static 'img/default.jpg' %}" class="display-image" alt="">
|
||||
<div class="float-left">
|
||||
<img src="{{ book.cover.url }}" class="display-image" alt="">
|
||||
</div>
|
||||
<div class="display-info">
|
||||
<h5 class="display-title">
|
||||
{{ book.title }}
|
||||
|
@ -88,7 +93,13 @@
|
|||
{% url 'users:home' book.last_editor %}
|
||||
{% endcomment %}
|
||||
|
||||
<div>{% trans '最近编辑者:' %}<a href="">someone</a></div>
|
||||
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.id %}">{{ book.last_editor | default:"" }}</a></div>
|
||||
<div>
|
||||
<a href="{% url 'books:update' book.id %}">{% trans '编辑这本书' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'books:delete' book.id %}" class="delete"> / {% trans '删除' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -104,15 +115,135 @@
|
|||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="set">
|
||||
<h5 class="set-title">{% trans '这本书的标记' %}</h5>
|
||||
{% if mark_list_more %}
|
||||
<a href="{% url 'books:retrieve_mark_list' book.id %}" class="more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
{% if mark_list %}
|
||||
{% for others_mark in mark_list %}
|
||||
<div class="mark">
|
||||
<div class="mark-label">
|
||||
<a href="{% url 'users:home' others_mark.owner.id %}" class="mark-owner-name">{{ others_mark.owner.username }}</a>
|
||||
<span>{{ others_mark.get_status_display }}</span>
|
||||
{% if others_mark.rating %}
|
||||
<span class="rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_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="mark-time">{{ others_mark.edited_time }}</span>
|
||||
</div>
|
||||
|
||||
{% if others_mark.text %}
|
||||
<p class="mark-text">{{ others_mark.text }}</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="set-empty">{% trans '暂无标记' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="set">
|
||||
<h5 class="set-title">{% trans '这本书的评论' %}</h5>
|
||||
{% if review_list_more %}
|
||||
<a href="{% url 'books:retrieve_review_list' book.id %}" class="more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
{% if review_list %}
|
||||
{% for others_review in review_list %}
|
||||
<div class="mark">
|
||||
<div class="mark-label">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="mark-owner-name">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.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="mark-time">{{ others_review.edited_time }}</span>
|
||||
<span class="review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="set-empty">{% trans '暂无评论' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aside">
|
||||
<div class="aside-card mast-user">
|
||||
<p class="info-brief mast-brief"></p>
|
||||
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||
<!-- <a href="#" class="report">{% trans '举报用户' %}</a> -->
|
||||
<div class="aside-card" id="asideMark">
|
||||
|
||||
{% if mark %}
|
||||
<div class="mark">
|
||||
<div class="clearfix">
|
||||
<span class="mark-status-label float-left">{% trans '我' %}{{ mark.get_status_display }}</span>
|
||||
{% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%}
|
||||
{% if mark.rating %}
|
||||
<span class="rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% 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="float-right">
|
||||
<a href="" class="edit">{% trans '修改' %}</a>
|
||||
<form action="{% url 'books:delete_mark' mark.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<a href="">{% trans '删除' %}</a>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<span class="mark-time float-left">{{ mark.edited_time }}</span>
|
||||
</div>
|
||||
{% if mark.text %}
|
||||
<p class="mark-text">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mark">
|
||||
<div class="clearfix">
|
||||
<span class="mark-status-label float-left">{% trans '标记这本书' %}</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<a href="" class="button" id="wishButton" data-status="{{ status_enum.WISH.value }}">{% trans '想看' %}</a>
|
||||
<a href="" class="button" data-status="{{ status_enum.DO.value }}">{% trans '在看' %}</a>
|
||||
<a href="" class="button" data-status="{{ status_enum.COLLECT.value }}">{% trans '看过' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="aside-card" id="asideReview">
|
||||
{% if review %}
|
||||
<div class="review">
|
||||
<div class="clearfix">
|
||||
<span class="review-label float-left">{% trans '我的评论' %}</span>
|
||||
{% if review.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="float-right">
|
||||
<a href="{% url 'books:update_review' review.id %}">{% trans '编辑' %}</a>
|
||||
<a href="{% url 'books:delete_review' review.id %}">{% trans '删除' %}</a>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<span class="review-time float-left">{{ review.edited_time }}</span>
|
||||
</div>
|
||||
<a href="{% url 'books:retrieve_review' review.id %}" class="review-title">
|
||||
{{ review.title }}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="clearfix">
|
||||
<span class="review-label float-left">{% trans '我的评论' %}</span>
|
||||
</div>
|
||||
<a href="{% url 'books:create_review' book.id %}" class="button">{% trans '去写评论' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
@ -124,6 +255,73 @@
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<div id="modals">
|
||||
<div class="modal mark-modal">
|
||||
<div class="modal-head clearfix">
|
||||
|
||||
{% if not mark %}
|
||||
<style>
|
||||
.modal-title::after {
|
||||
content: "{% trans '这本书' %}";
|
||||
}
|
||||
</style>
|
||||
<span class="modal-title float-left"></span>
|
||||
{% else %}
|
||||
<span class="modal-title float-left">{% trans '我的标记' %}</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="modal-close float-right">
|
||||
<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"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'books:create_update_mark' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ mark_form.id }}
|
||||
{{ mark_form.book }}
|
||||
{% if mark.rating %}
|
||||
{% endif %}
|
||||
<div class="clearfix">
|
||||
|
||||
<div class="rating-star-edit float-left"></div>
|
||||
<div class="modal-selection float-right" {% if not mark %}hidden{% endif %}>
|
||||
{{ mark_form.status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ mark_form.rating }}
|
||||
{{ mark_form.text }}
|
||||
<div class="modal-option clearfix">
|
||||
<div class="modal-selection float-left">
|
||||
<span>{{ mark_form.is_private.label }}:</span>
|
||||
{{ mark_form.is_private }}
|
||||
</div>
|
||||
<div class="modal-checkbox float-right">
|
||||
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix modal-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '提交' %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal confirm-modal">
|
||||
<div class="modal-head clearfix">
|
||||
<span class="modal-title float-left">{% trans '确定要删除你的标记吗?' %}</span>
|
||||
<span class="modal-close float-right">
|
||||
<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"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="clearfix modal-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '确认' %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-mask"></div>
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
|
|
0
books/templates/books/list.html
Normal file
0
books/templates/books/list.html
Normal file
177
books/templates/books/mark_list.html
Normal file
177
books/templates/books/mark_list.html
Normal file
|
@ -0,0 +1,177 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 搜索结果' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/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_browse.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<input type="search" class="search-box" name="keywords"
|
||||
value="{% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<h5 class="list-head">
|
||||
<a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>{% trans ' 的标记' %}
|
||||
</h5>
|
||||
<ul class="result-items set">
|
||||
|
||||
{% for mark in marks %}
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-label">
|
||||
<a href="{% url 'users:home' mark.owner.id %}" class="list-owner-name">{{ mark.owner.username }}</a>
|
||||
<span>{{ mark.get_status_display }}</span>
|
||||
{% if mark.rating %}
|
||||
<span class="rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></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="item-time">{{ mark.edited_time }}</span>
|
||||
</div>
|
||||
|
||||
{% if mark.text %}
|
||||
<p class="mark-text">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% empty %}
|
||||
{% trans '无结果' %}
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
<div class="pagination" >
|
||||
|
||||
<a
|
||||
{% if marks.has_previous %}
|
||||
href="?page=1"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not marks.has_previous %}disabled{% endif %} class="button button-clear">{% trans "首页" %}</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
{% if marks.has_previous %}
|
||||
href="?page={{ marks.previous_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not marks.has_previous %}disabled{% endif %} class="button button-clear">{% trans "上一页" %}</button>
|
||||
</a>
|
||||
|
||||
<span class="page-index">
|
||||
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
|
||||
</span>
|
||||
|
||||
<a
|
||||
{% if marks.has_next %}
|
||||
href="?page={{ marks.next_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}
|
||||
>
|
||||
<button {% if not marks.has_next %}disabled{% endif %} class="button button-clear">{% trans "下一页" %}</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
{% if marks.has_next %}
|
||||
href="?page={{ marks.paginator.num_pages }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not marks.has_next %}disabled{% endif %} class="button button-clear">{% trans "末页" %}</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aside">
|
||||
<div class="aside-card">
|
||||
<div class="aside-item">
|
||||
<a href="{% url 'books:retrieve' book.id %}"><img src="{{ book.cover.url }}" alt="" class="item-image"></a>
|
||||
<h5 class="item-title"><a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a></h5>
|
||||
|
||||
{% if book.isbn %}
|
||||
<div>ISBN: {{ book.isbn }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div>{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}</div>
|
||||
{% if book.rating %}
|
||||
{% trans '评分: ' %}<span class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
ratingLabels = $("#aside .rating-star");
|
||||
$(ratingLabels).each( function(index, value) {
|
||||
let ratingScore = $(this).data("rating-score") / 2;
|
||||
$(this).starRating({
|
||||
initialRating: ratingScore,
|
||||
readOnly: true,
|
||||
starSize: 15,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
142
books/templates/books/review_detail.html
Normal file
142
books/templates/books/review_detail.html
Normal file
|
@ -0,0 +1,142 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 查看评论' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<input type="search" class="search-box" name="keywords" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
|
||||
|
||||
<div class="review">
|
||||
<div class="review-head clearfix">
|
||||
<div class="mark-label float-left">
|
||||
<h4>
|
||||
{{ review.title }}
|
||||
{% if review.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 %}
|
||||
</h4>
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="mark-owner-name">{{ review.owner.username }}</a>
|
||||
|
||||
{% if mark %}
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<span class="mark-time">{{ review.edited_time }}</span>
|
||||
</div>
|
||||
|
||||
{% if request.user == review.owner %}
|
||||
<div class="edit float-right"><a href="{% url 'books:update_review' review.id %}">{% trans '编辑' %}</a></div>
|
||||
<div class="edit float-right"><a href="{% url 'books:delete_review' review.id %}">{% trans '删除' %}</a></div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
<div id="rawContent">
|
||||
{{ form.content }}
|
||||
</div>
|
||||
{{ form.media }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="aside">
|
||||
<div class="aside-card">
|
||||
<div class="aside-item">
|
||||
<a href="{% url 'books:retrieve' book.id %}"><img src="{{ book.cover.url }}" alt="" class="item-image"></a>
|
||||
<h5 class="item-title"><a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a></h5>
|
||||
|
||||
{% if book.isbn %}
|
||||
<div>ISBN: {{ book.isbn }}</div>
|
||||
{% endif %}
|
||||
<div>{% if book.author %}{% trans '作者:' %}
|
||||
{% for author in book.author %}
|
||||
<span>{{ author }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}</div>
|
||||
<div>{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}</div>
|
||||
<div>{%if book.pub_year %}{% trans '出版时间:' %}{{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{ book.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
|
||||
|
||||
{% if book.rating %}
|
||||
{% trans '评分: ' %}<span class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
ratingLabels = $("#aside .rating-star");
|
||||
$(ratingLabels).each( function(index, value) {
|
||||
let ratingScore = $(this).data("rating-score") / 2;
|
||||
$(this).starRating({
|
||||
initialRating: ratingScore,
|
||||
readOnly: true,
|
||||
starSize: 15,
|
||||
});
|
||||
});
|
||||
$(".markdownx textarea").hide();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
172
books/templates/books/review_list.html
Normal file
172
books/templates/books/review_list.html
Normal file
|
@ -0,0 +1,172 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 搜索结果' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/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_browse.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<input type="search" class="search-box" name="keywords"
|
||||
value="{% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<h5 class="list-head">
|
||||
<a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>{% trans ' 的评论' %}
|
||||
</h5>
|
||||
<ul class="result-items set">
|
||||
|
||||
{% for review in reviews %}
|
||||
|
||||
<div class="list-item">
|
||||
<div class="item-label">
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="item-owner-name">{{ review.owner.username }}</a>
|
||||
{% if review.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="item-time">{{ review.edited_time }}</span>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'books:retrieve_review' review.id %}" class="review-title">{{ review.title }}</a>
|
||||
|
||||
|
||||
</div>
|
||||
{% empty %}
|
||||
{% trans '无结果' %}
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
<div class="pagination" >
|
||||
|
||||
<a
|
||||
{% if reviews.has_previous %}
|
||||
href="?page=1"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not reviews.has_previous %}disabled{% endif %} class="button button-clear">{% trans "首页" %}</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
{% if reviews.has_previous %}
|
||||
href="?page={{ reviews.previous_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not reviews.has_previous %}disabled{% endif %} class="button button-clear">{% trans "上一页" %}</button>
|
||||
</a>
|
||||
|
||||
<span class="page-index">
|
||||
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
|
||||
</span>
|
||||
|
||||
<a
|
||||
{% if reviews.has_next %}
|
||||
href="?page={{ reviews.next_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}
|
||||
>
|
||||
<button {% if not reviews.has_next %}disabled{% endif %} class="button button-clear">{% trans "下一页" %}</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
{% if reviews.has_next %}
|
||||
href="?page={{ reviews.paginator.num_pages }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not reviews.has_next %}disabled{% endif %} class="button button-clear">{% trans "末页" %}</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aside">
|
||||
<div class="aside-card">
|
||||
<div class="aside-item">
|
||||
<a href="{% url 'books:retrieve' book.id %}"><img src="{{ book.cover.url }}" alt="" class="item-image"></a>
|
||||
<h5 class="item-title"><a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a></h5>
|
||||
|
||||
{% if book.isbn %}
|
||||
<div>ISBN: {{ book.isbn }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div>{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}</div>
|
||||
{% if book.rating %}
|
||||
{% trans '评分: ' %}<span class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
ratingLabels = $("#aside .rating-star");
|
||||
$(ratingLabels).each( function(index, value) {
|
||||
let ratingScore = $(this).data("rating-score") / 2;
|
||||
$(this).starRating({
|
||||
initialRating: ratingScore,
|
||||
readOnly: true,
|
||||
starSize: 15,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
91
books/templates/books/scrape.html
Normal file
91
books/templates/books/scrape.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 主页' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'js/scrape.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
#scrapeForm label{
|
||||
font-size: small !important;
|
||||
}
|
||||
</style>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<input type="search" class="search-box" name="keywords" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if request.user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<iframe src="https://search.douban.com/book/subject_search{% if keywords %}?search_text={{ keywords }}{% endif %}" frameborder="0"></iframe>
|
||||
<div class="dividing-line"></div>
|
||||
<div id="scrapeForm">
|
||||
<form action="{% url 'books:create' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aside">
|
||||
<div class="aside-card">
|
||||
<div class="add-nav">
|
||||
|
||||
<div>
|
||||
|
||||
{% trans '根据豆瓣内容填写!' %}
|
||||
</div>
|
||||
<a href="{% url 'books:scrape' %}" id="submit" class="button add-button">{% trans '剽取!' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -6,4 +6,15 @@ app_name = 'books'
|
|||
urlpatterns = [
|
||||
path('create/', create, name='create'),
|
||||
path('<int:id>/', retrieve, name='retrieve'),
|
||||
path('update/<int:id>/', update, name='update'),
|
||||
path('delete/<int:id>/', delete, name='delete'),
|
||||
path('mark/', create_update_mark, name='create_update_mark'),
|
||||
path('<int:book_id>/mark/list/', retrieve_mark_list, name='retrieve_mark_list'),
|
||||
path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
|
||||
path('<int:book_id>/review/create/', create_review, name='create_review'),
|
||||
path('review/update/<int:id>/', update_review, name='update_review'),
|
||||
path('review/delete/<int:id>/', delete_review, name='delete_review'),
|
||||
path('review/<int:id>/', retrieve_review, name='retrieve_review'),
|
||||
path('<int:book_id>/review/list/', retrieve_review_list, name='retrieve_review_list'),
|
||||
path('scrape/', scrape, name='scrape'),
|
||||
]
|
||||
|
|
412
books/views.py
412
books/views.py
|
@ -1,11 +1,30 @@
|
|||
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.http import HttpResponseBadRequest, HttpResponseServerError
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from common.mastodon import mastodon_request_included
|
||||
from common.mastodon.api import check_visibility, post_toot, TootVisibilityEnum
|
||||
from .models import *
|
||||
from .forms import *
|
||||
from .forms import BookMarkStatusTranslator
|
||||
|
||||
|
||||
# how many marks showed on the detail page
|
||||
MARK_NUMBER = 5
|
||||
# how many marks at the mark page
|
||||
MARK_PER_PAGE = 20
|
||||
# how many reviews showed on the detail page
|
||||
REVIEW_NUMBER = 5
|
||||
# how many reviews at the mark page
|
||||
REVIEW_PER_PAGE = 20
|
||||
|
||||
|
||||
# public data
|
||||
###########################
|
||||
@login_required
|
||||
def create(request):
|
||||
if request.method == 'GET':
|
||||
|
@ -15,31 +34,404 @@ def create(request):
|
|||
'books/create_update.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _('添加书籍')
|
||||
'title': _('添加书籍'),
|
||||
'submit_url': reverse("books:create")
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
# check user credential in post data, must be the login user
|
||||
pass
|
||||
form = BookForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.instance.last_editor = request.user
|
||||
form.save()
|
||||
|
||||
return redirect(reverse("books:retrieve", args=[form.instance.id]))
|
||||
if request.user.is_authenticated:
|
||||
# only local user can alter public data
|
||||
form = BookForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
form.instance.last_editor = request.user
|
||||
form.save()
|
||||
return redirect(reverse("books:retrieve", args=[form.instance.id]))
|
||||
else:
|
||||
return render(
|
||||
request,
|
||||
'books/create_update.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _('添加书籍'),
|
||||
'submit_url': reverse("books:create")
|
||||
}
|
||||
)
|
||||
else:
|
||||
return redirect(reverse("users:login"))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def update(request, id):
|
||||
if request.method == 'GET':
|
||||
book = get_object_or_404(Book, pk=id)
|
||||
form = BookForm(instance=book)
|
||||
return render(
|
||||
request,
|
||||
'books/create_update.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _('修改书籍'),
|
||||
'submit_url': reverse("books:update", args=[book.id])
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
book = get_object_or_404(Book, pk=id)
|
||||
form = BookForm(request.POST, instance=book)
|
||||
if form.is_valid():
|
||||
form.instance.last_editor = request.user
|
||||
form.instance.edited_time = timezone.now()
|
||||
form.save()
|
||||
return redirect(reverse("books:retrieve", args=[form.instance.id]))
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def retrieve(request, id):
|
||||
if request.method == 'GET':
|
||||
book = get_object_or_404(Book, pk=id)
|
||||
mark = None
|
||||
review = None
|
||||
try:
|
||||
mark = BookMark.objects.get(owner=request.user, book=book)
|
||||
except ObjectDoesNotExist:
|
||||
mark = None
|
||||
if mark:
|
||||
mark.get_status_display = BookMarkStatusTranslator(mark.status)
|
||||
mark_form = BookMarkForm(instance=mark)
|
||||
else:
|
||||
mark_form = BookMarkForm(initial={
|
||||
'book': book
|
||||
})
|
||||
|
||||
try:
|
||||
review = BookReview.objects.get(owner=request.user, book=book)
|
||||
except ObjectDoesNotExist:
|
||||
review = None
|
||||
|
||||
mark_list = BookMark.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 = BookReview.get_available(book, request.user, request.session['oauth_token'])
|
||||
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('<.*?>')
|
||||
# return re.sub(regex, '', text)
|
||||
|
||||
# for r in review_list:
|
||||
# r.content = strip_html_tags(r.content)
|
||||
|
||||
return render(
|
||||
request,
|
||||
'books/detail.html',
|
||||
{
|
||||
'book': book,
|
||||
'mark': mark,
|
||||
'review': review,
|
||||
'status_enum': MarkStatusEnum,
|
||||
'mark_form': mark_form,
|
||||
'mark_list': mark_list,
|
||||
'mark_list_more': mark_list_more,
|
||||
'review_list': review_list,
|
||||
'review_list_more': review_list_more,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def delete(request, id):
|
||||
if request.method == 'GET':
|
||||
book = get_object_or_404(Book, pk=id)
|
||||
return render(
|
||||
request,
|
||||
'books/delete.html',
|
||||
{
|
||||
'book': book,
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
if request.user.is_staff:
|
||||
# only staff has right to delete
|
||||
book = get_object_or_404(Book, pk=id)
|
||||
book.delete()
|
||||
return redirect(reverse("common:search"))
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
# user owned entites
|
||||
###########################
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def create_update_mark(request):
|
||||
# check list:
|
||||
# clean rating if is wish
|
||||
# transaction on updating book rating
|
||||
# owner check(guarantee)
|
||||
if request.method == 'POST':
|
||||
pk = request.POST.get('id')
|
||||
old_rating = None
|
||||
if pk:
|
||||
mark = get_object_or_404(BookMark, pk=pk)
|
||||
old_rating = mark.rating
|
||||
# update
|
||||
form = BookMarkForm(request.POST, instance=mark)
|
||||
else:
|
||||
# create
|
||||
form = BookMarkForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
if form.instance.status == MarkStatusEnum.WISH.value:
|
||||
form.instance.rating = None
|
||||
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()
|
||||
except IntegrityError as e:
|
||||
return HttpResponseServerError()
|
||||
|
||||
if form.cleaned_data['share_to_mastodon']:
|
||||
if form.cleaned_data['is_private']:
|
||||
visibility = TootVisibilityEnum.PRIVATE
|
||||
else:
|
||||
visibility = TootVisibilityEnum.PUBLIC
|
||||
url = "https://" + request.get_host() + reverse("books:retrieve", args=[book.id])
|
||||
words = BookMarkStatusTranslator(int(form.cleaned_data['status'])) + f"《{book.title}》"
|
||||
content = words + '\n' + url + '\n' + form.cleaned_data['text']
|
||||
post_toot(content, visibility, request.session['oauth_token'])
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
return redirect(reverse("books:retrieve", args=[form.instance.book.id]))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
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'])
|
||||
paginator = Paginator(queryset, MARK_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
marks = paginator.get_page(page_number)
|
||||
for m in marks:
|
||||
m.get_status_display = BookMarkStatusTranslator(m.status)
|
||||
return render(
|
||||
request,
|
||||
'books/mark_list.html',
|
||||
{
|
||||
'marks': marks,
|
||||
'book': book,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_mark(request, id):
|
||||
if request.method == 'POST':
|
||||
mark = get_object_or_404(BookMark, pk=id)
|
||||
book_id = mark.book.id
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# update book rating
|
||||
mark.book.update_rating(mark.rating, None)
|
||||
mark.delete()
|
||||
except IntegrityError as e:
|
||||
return HttpResponseServerError()
|
||||
return redirect(reverse("books:retrieve", args=[book_id]))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def create_review(request, book_id):
|
||||
if request.method == 'GET':
|
||||
form = BookReviewForm(initial={'book': book_id})
|
||||
book = get_object_or_404(Book, pk=book_id)
|
||||
return render(
|
||||
request,
|
||||
'books/create_update_review.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _("添加评论"),
|
||||
'book': book,
|
||||
'submit_url': reverse("books:create_review", args=[book_id]),
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
form = BookReviewForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.instance.owner = request.user
|
||||
form.save()
|
||||
if form.cleaned_data['share_to_mastodon']:
|
||||
if form.cleaned_data['is_private']:
|
||||
visibility = TootVisibilityEnum.PRIVATE
|
||||
else:
|
||||
visibility = TootVisibilityEnum.PUBLIC
|
||||
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']
|
||||
post_toot(content, visibility, request.session['oauth_token'])
|
||||
return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def update_review(request, id):
|
||||
# owner check
|
||||
# edited time
|
||||
if request.method == 'GET':
|
||||
review = get_object_or_404(BookReview, pk=id)
|
||||
if request.user != review.owner:
|
||||
return HttpResponseBadRequest()
|
||||
form = BookReviewForm(instance=review)
|
||||
book = review.book
|
||||
return render(
|
||||
request,
|
||||
'books/create_update_review.html',
|
||||
{
|
||||
'form': form,
|
||||
'title': _("编辑评论"),
|
||||
'book': book,
|
||||
'submit_url': reverse("books:update_review", args=[review.id]),
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
review = get_object_or_404(BookReview, pk=id)
|
||||
if request.user != review.owner:
|
||||
return HttpResponseBadRequest()
|
||||
form = BookReviewForm(request.POST, instance=review)
|
||||
if form.is_valid():
|
||||
form.instance.edited_time = timezone.now()
|
||||
form.save()
|
||||
if form.cleaned_data['share_to_mastodon']:
|
||||
if form.cleaned_data['is_private']:
|
||||
visibility = TootVisibilityEnum.PRIVATE
|
||||
else:
|
||||
visibility = TootVisibilityEnum.PUBLIC
|
||||
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']
|
||||
post_toot(content, visibility, request.session['oauth_token'])
|
||||
return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_review(request, id):
|
||||
if request.method == 'GET':
|
||||
review = get_object_or_404(BookReview, pk=id)
|
||||
if request.user != review.owner:
|
||||
return HttpResponseBadRequest()
|
||||
review_form = BookReviewForm(instance=review)
|
||||
return render(
|
||||
request,
|
||||
'books/delete_review.html',
|
||||
{
|
||||
'form': review_form,
|
||||
'review': review,
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
review = get_object_or_404(BookReview, pk=id)
|
||||
if request.user != review.owner:
|
||||
return HttpResponseBadRequest()
|
||||
book_id = review.book.id
|
||||
review.delete()
|
||||
return redirect(reverse("books:retrieve", args=[book_id]))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def retrieve_review(request, id):
|
||||
if request.method == 'GET':
|
||||
review = get_object_or_404(BookReview, pk=id)
|
||||
if not check_visibility(review, request.session['oauth_token'], request.user):
|
||||
return HttpResponseBadRequest()
|
||||
review_form = BookReviewForm(instance=review)
|
||||
book = review.book
|
||||
try:
|
||||
mark = BookMark.objects.get(owner=review.owner, book=book)
|
||||
mark.get_status_display = BookMarkStatusTranslator(mark.status)
|
||||
except ObjectDoesNotExist:
|
||||
mark = None
|
||||
return render(
|
||||
request,
|
||||
'books/review_detail.html',
|
||||
{
|
||||
'form': review_form,
|
||||
'review': review,
|
||||
'book': book,
|
||||
'mark': mark,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
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'])
|
||||
paginator = Paginator(queryset, REVIEW_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
reviews = paginator.get_page(page_number)
|
||||
return render(
|
||||
request,
|
||||
'books/review_list.html',
|
||||
{
|
||||
'reviews': reviews,
|
||||
'book': book,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def scrape(request):
|
||||
if request.method == 'GET':
|
||||
keywords = request.GET.get('keywords')
|
||||
form = BookForm()
|
||||
return render(
|
||||
request,
|
||||
'books/scrape.html',
|
||||
{
|
||||
'keywords': keywords,
|
||||
'form': form,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django import forms
|
||||
from django.contrib.postgres.forms import JSONField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import json
|
||||
|
||||
|
||||
|
@ -22,9 +24,36 @@ class KeyValueInput(forms.Widget):
|
|||
if context['widget']['value']:
|
||||
key_value_pairs = json.loads(context['widget']['value'])
|
||||
# for kv in key_value_pairs:
|
||||
|
||||
context['widget']['keyvalue_pairs'] = {
|
||||
|
||||
}
|
||||
context['widget']['keyvalue_pairs'] = key_value_pairs
|
||||
return context
|
||||
|
||||
|
||||
class RadioBooleanField(forms.ChoiceField):
|
||||
widget = forms.RadioSelect
|
||||
|
||||
def to_python(self, value):
|
||||
"""Return a Python boolean object."""
|
||||
# Explicitly check for the string 'False', which is what a hidden field
|
||||
# will submit for False. Also check for '0', since this is what
|
||||
# RadioSelect will provide. Because bool("True") == bool('1') == True,
|
||||
# we don't need to handle that explicitly.
|
||||
if isinstance(value, str) and value.lower() in ('false', '0'):
|
||||
value = False
|
||||
else:
|
||||
value = bool(value)
|
||||
return super().to_python(value)
|
||||
|
||||
|
||||
class RatingValidator:
|
||||
""" empty value is not validated """
|
||||
def __call__(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise ValidationError(
|
||||
_('%(value)s is not an integer'),
|
||||
params={'value': value},
|
||||
)
|
||||
if not str(value) in [str(i) for i in range(1, 11)]:
|
||||
raise ValidationError(
|
||||
_('%(value)s is not an integer in range 1-10'),
|
||||
params={'value': value},
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
from .decorators import *
|
|
@ -1,22 +1,91 @@
|
|||
import requests
|
||||
import string
|
||||
import random
|
||||
import functools
|
||||
from boofilsic.settings import MASTODON_TIMEOUT, MASTODON_DOMAIN_NAME
|
||||
|
||||
# See https://docs.joinmastodon.org/methods/accounts/
|
||||
|
||||
# returns user info
|
||||
# retruns the same info as verify account credentials
|
||||
# GET
|
||||
ACCOUNT = '/api/v1/accounts/:id'
|
||||
API_GET_ACCOUNT = '/api/v1/accounts/:id'
|
||||
|
||||
# returns user info if valid, 401 if invalid
|
||||
# GET
|
||||
VERIFY_ACCOUNT_CREDENTIALS = '/api/v1/accounts/verify_credentials'
|
||||
API_VERIFY_ACCOUNT = '/api/v1/accounts/verify_credentials'
|
||||
|
||||
# obtain token
|
||||
# GET
|
||||
OAUTH_TOKEN = '/oauth/token'
|
||||
API_OBTAIN_TOKEN = '/oauth/token'
|
||||
|
||||
# obatin auth code
|
||||
# GET
|
||||
OAUTH_AUTHORIZE = '/oauth/authorize'
|
||||
API_OAUTH_AUTHORIZE = '/oauth/authorize'
|
||||
|
||||
# revoke token
|
||||
# POST
|
||||
REVOKE_TOKEN = '/oauth/revoke'
|
||||
API_REVOKE_TOKEN = '/oauth/revoke'
|
||||
|
||||
# relationships
|
||||
# GET
|
||||
API_GET_RELATIONSHIPS = '/api/v1/accounts/relationships'
|
||||
|
||||
# toot
|
||||
# POST
|
||||
API_PUBLISH_TOOT = '/api/v1/statuses'
|
||||
|
||||
|
||||
get = functools.partial(requests.get, timeout=MASTODON_TIMEOUT)
|
||||
post = functools.partial(requests.post, timeout=MASTODON_TIMEOUT)
|
||||
|
||||
|
||||
def get_relationships(id_list, token):
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + API_GET_RELATIONSHIPS
|
||||
payload = {'id[]': id_list}
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
response = get(url, headers=headers, data=payload)
|
||||
return response.json()
|
||||
|
||||
|
||||
def check_visibility(user_owned_entity, token, visitor):
|
||||
"""
|
||||
check if given user can see the user owned entity
|
||||
"""
|
||||
if not visitor == user_owned_entity.owner:
|
||||
# mastodon request
|
||||
relationship = get_relationships([visitor.mastodon_id], token)[0]
|
||||
if relationship['blocked_by']:
|
||||
return False
|
||||
if not relationship['following'] and user_owned_entity.is_private:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def post_toot(content, visibility, token):
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + API_PUBLISH_TOOT
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Idempotency-Key': random_string_generator(16)
|
||||
}
|
||||
payload = {
|
||||
'status': content,
|
||||
'visibility': visibility
|
||||
}
|
||||
response = post(url, headers=headers, data=payload)
|
||||
return response
|
||||
|
||||
def random_string_generator(n):
|
||||
s = string.ascii_letters + string.punctuation + string.digits
|
||||
return ''.join(random.choice(s) for i in range(n))
|
||||
|
||||
|
||||
class TootVisibilityEnum:
|
||||
PUBLIC = 'public'
|
||||
PRIVATE = 'private'
|
||||
DIRECT = 'direct'
|
||||
UNLISTED = 'unlisted'
|
87
common/mastodon/auth.py
Normal file
87
common/mastodon/auth.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
from django.contrib.auth.backends import ModelBackend, UserModel
|
||||
from django.shortcuts import reverse
|
||||
from boofilsic.settings import MASTODON_DOMAIN_NAME, CLIENT_ID, CLIENT_SECRET
|
||||
from .api import *
|
||||
|
||||
|
||||
def obtain_token(request, code):
|
||||
""" Returns token if success else None. """
|
||||
payload = {
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'redirect_uri': f"http://{request.get_host()}{reverse('users:OAuth2_login')}",
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'scope': 'read write'
|
||||
}
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + API_OBTAIN_TOKEN
|
||||
response = post(url, data=payload)
|
||||
if response.status_code != 200:
|
||||
return
|
||||
data = response.json()
|
||||
return data.get('access_token')
|
||||
|
||||
|
||||
def get_user_data(token):
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + API_VERIFY_ACCOUNT
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
response = get(url, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def revoke_token(token):
|
||||
payload = {
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'scope': token
|
||||
}
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + API_REVOKE_TOKEN
|
||||
response = post(url, data=payload)
|
||||
|
||||
|
||||
def verify_token(token):
|
||||
""" Check if the token is valid and is of local instance. """
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + API_VERIFY_ACCOUNT
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
response = get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
res_data = response.json()
|
||||
# check if is local instance user
|
||||
if res_data['acct'] == res_data['username']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class OAuth2Backend(ModelBackend):
|
||||
""" Used to glue OAuth2 and Django User model """
|
||||
# "authenticate() should check the credentials it gets and returns
|
||||
# a user object that matches those credentials."
|
||||
# arg request is an interface specification, not used in this implementation
|
||||
def authenticate(self, request, token=None, username=None, **kwargs):
|
||||
""" when username is provided, assume that token is newly obtained and valid """
|
||||
if token is None:
|
||||
return
|
||||
|
||||
if username is None:
|
||||
user_data = get_user_data(token)
|
||||
if user_data:
|
||||
username = user_data['username']
|
||||
else:
|
||||
# aquiring user data fail means token is invalid thus auth fail
|
||||
return None
|
||||
|
||||
# when username is provided, assume that token is newly obtained and valid
|
||||
try:
|
||||
user = UserModel._default_manager.get_by_natural_key(user_data['username'])
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
else:
|
||||
if self.user_can_authenticate(user):
|
||||
return user
|
||||
return None
|
34
common/mastodon/decorators.py
Normal file
34
common/mastodon/decorators.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.http import HttpResponse
|
||||
import functools
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from requests.exceptions import Timeout
|
||||
|
||||
|
||||
def mastodon_request_included(func):
|
||||
""" Handles timeout exception of requests to mastodon, returns http 500 """
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Timeout:
|
||||
return render(
|
||||
args[0],
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': _("长毛象请求超时叻_(´ཀ`」 ∠)__ ")
|
||||
}
|
||||
)
|
||||
except ConnectionError:
|
||||
return render(
|
||||
args[0],
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': _("长毛象请求超时叻_(´ཀ`」 ∠)__ ")
|
||||
}
|
||||
)
|
||||
return wrapper
|
||||
|
||||
|
||||
class HttpResponseInternalServerError(HttpResponse):
|
||||
status_code = 500
|
|
@ -1,10 +1,12 @@
|
|||
import django.contrib.postgres.fields as postgres
|
||||
from decimal import *
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
from django.db import models, IntegrityError
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Q
|
||||
from markdownx.models import MarkdownxField
|
||||
from users.models import User
|
||||
from common.mastodon.api import get_relationships
|
||||
|
||||
|
||||
# abstract base classes
|
||||
|
@ -13,7 +15,7 @@ class Resource(models.Model):
|
|||
|
||||
rating_total_score = models.PositiveIntegerField(null=True, blank=True)
|
||||
rating_number = models.PositiveIntegerField(null=True, blank=True)
|
||||
rating = models.DecimalField(null=True, blank=True, max_digits=2, decimal_places=1)
|
||||
rating = models.DecimalField(null=True, blank=True, max_digits=3, decimal_places=1)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now_add=True)
|
||||
last_editor = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='%(class)s_last_editor', null=True, blank=False)
|
||||
|
@ -30,32 +32,115 @@ class Resource(models.Model):
|
|||
def save(self, *args, **kwargs):
|
||||
""" update rating before save to db """
|
||||
if self.rating_number and self.rating_total_score:
|
||||
self.rating = Decimal(str(round(self.rating_total_score / self.rating_number ), 1))
|
||||
self.rating = Decimal(str(round(self.rating_total_score / self.rating_number, 1)))
|
||||
elif self.rating_number is None and self.rating_total_score is None:
|
||||
self.rating = None
|
||||
else:
|
||||
raise IntegrityError()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def calculate_rating(self, old_rating, new_rating):
|
||||
if (not (self.rating and self.rating_total_score and self.rating_number)\
|
||||
and (self.rating or self.rating_total_score or self.rating_number))\
|
||||
or (not (self.rating or self.rating_number or self.rating_total_score) and old_rating is not None):
|
||||
raise IntegrityError("Rating integiry error.")
|
||||
if old_rating:
|
||||
if new_rating:
|
||||
# old -> new
|
||||
self.rating_total_score += (new_rating - old_rating)
|
||||
else:
|
||||
# old -> none
|
||||
if self.rating_number >= 2:
|
||||
self.rating_total_score -= old_rating
|
||||
self.rating_number -= 1
|
||||
else:
|
||||
# only one rating record
|
||||
self.rating_number = None
|
||||
self.rating_total_score = None
|
||||
pass
|
||||
else:
|
||||
if new_rating:
|
||||
# none -> new
|
||||
if self.rating_number and self.rating_number >= 1:
|
||||
self.rating_total_score += new_rating
|
||||
self.rating_number += 1
|
||||
else:
|
||||
# no rating record before
|
||||
self.rating_number = 1
|
||||
self.rating_total_score = new_rating
|
||||
else:
|
||||
# none -> none
|
||||
pass
|
||||
|
||||
def update_rating(self, old_rating, new_rating):
|
||||
self.calculate_rating(old_rating, new_rating)
|
||||
self.save()
|
||||
|
||||
|
||||
class UserOwnedEntity(models.Model):
|
||||
is_private = models.BooleanField()
|
||||
owner = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='user_%(class)ss', null=True)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_%(class)ss')
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def get_available(cls, resource, user, token):
|
||||
"""
|
||||
Returns all avaliable user-owned entities related to given resource.
|
||||
This method handls mute/block relationships and private/public visibilities.
|
||||
"""
|
||||
# the foreign key field that points to resource
|
||||
# has to be named as the lower case name of that resource
|
||||
query_kwargs = {resource.__class__.__name__.lower(): resource}
|
||||
user_owned_entities = cls.objects.filter(**query_kwargs).order_by("-edited_time")
|
||||
# every user should only be abled to have one user owned entity for each resource
|
||||
# this is guaranteed by models
|
||||
id_list = [e.owner.mastodon_id for e in user_owned_entities]
|
||||
# Mastodon request
|
||||
# relationships = get_relationships(id_list, token)
|
||||
# mute_block_blocked = []
|
||||
# following = []
|
||||
# for r in relationships:
|
||||
# # check json data type
|
||||
# if r['blocking'] or r['blocked_by'] or r['muting']:
|
||||
# mute_block_blocked.append(r['id'])
|
||||
# if r['following']:
|
||||
# following.append(r['id'])
|
||||
# user_owned_entities = user_owned_entities.exclude(owner__mastodon_id__in=mute_block_blocked)
|
||||
# following.append(str(user.mastodon_id))
|
||||
# user_owned_entities = user_owned_entities.exclude(Q(is_private=True) & ~Q(owner__mastodon_id__in=following))
|
||||
return user_owned_entities
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_available_user_data(cls, owner, is_following):
|
||||
"""
|
||||
Returns all avaliable owner's entities.
|
||||
|
||||
:param owner: visited user
|
||||
:param is_following: if the current user is following the owner
|
||||
"""
|
||||
user_owned_entities = cls.objects.filter(owner=owner)
|
||||
if not is_following:
|
||||
user_owned_entities = user_owned_entities.exclude(is_private=True)
|
||||
return user_owned_entities
|
||||
|
||||
|
||||
# commonly used entity classes
|
||||
###################################
|
||||
class MarkStatusEnum(models.IntegerChoices):
|
||||
DO = 1, _('Do')
|
||||
WISH = 2, _('Wish')
|
||||
WISH = 1, _('Wish')
|
||||
DO = 2, _('Do')
|
||||
COLLECT = 3, _('Collect')
|
||||
|
||||
|
||||
class Mark(UserOwnedEntity):
|
||||
status = models.SmallIntegerField(choices=MarkStatusEnum.choices)
|
||||
rating = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
text = models.CharField(max_length=150, blank=True, default='')
|
||||
text = models.CharField(max_length=500, blank=True, default='')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
|
29
common/static/css/boofilsic_box.css
Normal file
29
common/static/css/boofilsic_box.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
.box {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 80px 100px;
|
||||
padding-bottom: 60px;
|
||||
background-color: var(--bright);
|
||||
text-align: center;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.box .sec-msg {
|
||||
color: var(--light);
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.box .main-msg {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.box .logo {
|
||||
width: 140px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.box p {
|
||||
text-align: justify;
|
||||
}
|
|
@ -18,9 +18,17 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
color: var(--primary);
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
#page-wrapper {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
|
@ -78,6 +86,126 @@ div.dividing-line {
|
|||
border-top: solid 1px var(--light);
|
||||
}
|
||||
|
||||
.icon-lock svg{
|
||||
fill: var(--light);
|
||||
height: 12px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
transform: scale(0.4);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.lds-spinner {
|
||||
color: official;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.lds-spinner div {
|
||||
transform-origin: 40px 40px;
|
||||
animation: lds-spinner 1.2s linear infinite;
|
||||
}
|
||||
|
||||
.lds-spinner div:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 37px;
|
||||
width: 6px;
|
||||
height: 18px;
|
||||
border-radius: 20%;
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -1s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
|
||||
.lds-spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* overwrite rating-star plugin makes it readonly */
|
||||
.rating-star .jq-star {
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
/* Nav Bar */
|
||||
|
||||
|
@ -126,6 +254,7 @@ section#content div#main {
|
|||
background-color: var(--bright);
|
||||
margin-right: 40px;
|
||||
width: 75%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Aside Content Section */
|
||||
|
@ -134,8 +263,13 @@ section#content div#aside {
|
|||
width: 25%;
|
||||
}
|
||||
|
||||
.set {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.set .set-title {
|
||||
display: inline-block;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.set .set-empty {
|
||||
font-size: small;
|
||||
|
@ -144,11 +278,12 @@ section#content div#aside {
|
|||
}
|
||||
|
||||
.set .set-item-list {
|
||||
padding: 0 25px 0 10px;
|
||||
padding: 8px 25px 0 10px;
|
||||
}
|
||||
|
||||
.set .set-item {
|
||||
text-align: center;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.set .set-item-image {
|
||||
|
@ -158,7 +293,7 @@ section#content div#aside {
|
|||
}
|
||||
|
||||
.set .set-item-title, .set .set-item-title:visited {
|
||||
width: 80%;
|
||||
/* width: 80%; */
|
||||
margin: auto;
|
||||
line-height: 1.3em;
|
||||
font-size: small;
|
||||
|
@ -250,6 +385,10 @@ section#content div#aside {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.relation-user .relation-name .emoji {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.relation-card .relation-user-list {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
@ -283,6 +422,10 @@ section#content div#aside {
|
|||
|
||||
/* Search Result */
|
||||
|
||||
.result-items .result-item:last-of-type {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
list-style: none;
|
||||
margin-bottom: 30px;
|
||||
|
@ -313,11 +456,6 @@ section#content div#aside {
|
|||
left: -3px;
|
||||
}
|
||||
|
||||
/* overwrite rating-star plugin */
|
||||
.result-item .result-info .rating-star .jq-star {
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
.result-item .result-info .result-book-info {
|
||||
font-size: 80%;
|
||||
overflow: hidden;
|
||||
|
@ -335,7 +473,7 @@ section#content div#aside {
|
|||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.result-item .result-info .result-book-title {
|
||||
.result-item .result-info .result-item-title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
|
@ -343,7 +481,7 @@ section#content div#aside {
|
|||
overflow: hidden
|
||||
}
|
||||
|
||||
.result-item .result-info .result-book-brief {
|
||||
.result-item .result-info .result-item-brief {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
font-size: small;
|
||||
|
@ -355,9 +493,16 @@ section#content div#aside {
|
|||
height: 90px;
|
||||
}
|
||||
|
||||
.aside-card .add-nav .clearfix {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
margin-top: 50px;
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -413,4 +558,176 @@ img.display-image {
|
|||
.display-info-detail .rating-score {
|
||||
position: relative;
|
||||
top: -2.5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.display-info-detail form, .display-info-detail input[type='submit'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.mark {
|
||||
font-size: small;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mark form {
|
||||
display: inline;
|
||||
/* margin: 0; */
|
||||
}
|
||||
|
||||
.mark .mark-status-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mark .rating-star {
|
||||
position: relative;
|
||||
top: 1.5px;
|
||||
}
|
||||
|
||||
.mark .icon-lock {
|
||||
position: relative;
|
||||
top: -0.5px;
|
||||
}
|
||||
|
||||
.mark .clearfix {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.list-item .item-time {
|
||||
color: var(--light);
|
||||
margin-left: 5px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.list-item .mark-text {
|
||||
margin: 20px 0 15px 0;
|
||||
}
|
||||
|
||||
|
||||
.mark .button-group a.button{
|
||||
width: 33%;
|
||||
box-sizing: border-box;
|
||||
display: initial;
|
||||
padding: 7px 8%;
|
||||
margin: 0 4px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.review {
|
||||
font-size: small;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.review .clearfix {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.review .review-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.review .review-time {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.review .review-title {
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.review .review-head .edit {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.set .mark {
|
||||
font-size: small;
|
||||
/* margin-bottom: 3px; */
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.list-label .rating-star {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.list-label .list-owner-name {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.list-label {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.set .mark-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.set .review-title {
|
||||
font-weight: bold;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.set.result-items .list-item {
|
||||
margin-bottom: 10px;
|
||||
}s
|
||||
|
||||
.aside-card .aside-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aside-item .item-image {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.aside-item div {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.aside-item .item-title {
|
||||
font-weight: bold;
|
||||
font-size: small;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.aside-item .rating-star {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.list-head {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-size: small;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.user .user-info {
|
||||
margin-left: 15px;
|
||||
max-width: 88%;
|
||||
}
|
||||
|
||||
.user .user-name {
|
||||
font-size: small;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.user .user-brief {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user .avatar {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
|
@ -18,9 +18,33 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
.rating-star .jq-star {
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
.icon-lock svg{
|
||||
fill: var(--light);
|
||||
height: 12px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
color: var(--primary);
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
#page-wrapper {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
|
@ -53,6 +77,11 @@ footer a {
|
|||
font-size: small;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 20px !important;
|
||||
width: 20px !important;
|
||||
|
@ -67,6 +96,12 @@ img.emoji {
|
|||
margin-left: 5px;
|
||||
}
|
||||
|
||||
div.dividing-line {
|
||||
height: 0;
|
||||
width: 100%;
|
||||
margin: 40px 0 15px 0;
|
||||
border-top: solid 1px var(--light);
|
||||
}
|
||||
|
||||
/* Nav Bar */
|
||||
|
||||
|
@ -92,7 +127,7 @@ section#navbar {
|
|||
}
|
||||
|
||||
.navbar a.nav-link, .navbar a.nav-link:visited {
|
||||
font-size: 80%;
|
||||
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
color: var(--secondary);
|
||||
|
@ -103,6 +138,10 @@ section#navbar {
|
|||
color: var(--primary);
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
/* Main Content Section */
|
||||
|
||||
section#content div#main {
|
||||
|
@ -235,95 +274,88 @@ div#main form input[type="file"] {
|
|||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* Search Result */
|
||||
|
||||
.result-item {
|
||||
list-style: none;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
.result-item .result-info {
|
||||
margin-left: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.result-item img.result-book-cover {
|
||||
float: left;
|
||||
object-fit: contain;
|
||||
.item-card img.item-image {
|
||||
height: 150px;
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.result-item .result-info .rating-empty {
|
||||
font-size: small;
|
||||
.item-card .item-info {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.result-item .result-info .rating-star{
|
||||
cursor: unset;
|
||||
.item-card .item-info .item-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-card .item-info .rating-star {
|
||||
display: inline;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
left: -3px;
|
||||
left: -2px;
|
||||
}
|
||||
|
||||
/* overwrite rating-star plugin */
|
||||
.result-item .result-info .rating-star .jq-star {
|
||||
cursor: unset;
|
||||
.prompt {
|
||||
font-weight: bold;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.result-item .result-info .result-book-info {
|
||||
font-size: 80%;
|
||||
overflow: hidden;
|
||||
width: 420px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
top: .55rem;
|
||||
}
|
||||
|
||||
.result-item .result-info .rating-score {
|
||||
font-size: 80%;
|
||||
.review .option .selection ul {
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.result-item .result-info .result-book-title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
display: block;
|
||||
overflow: hidden
|
||||
.review .option .selection ul li, .review .option .selection ul label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.result-item .result-info .result-book-brief {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
.review .option .selection ul li {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.review .option input[type="checkbox"], .review .option input[type="radio"] {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.review input[type="submit"] {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.review textarea {
|
||||
height: 400px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.review .preview-button {
|
||||
font-size: small;
|
||||
width: 565px;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
/* white-space: nowrap; */
|
||||
height: 90px;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
.review .markdownx-preview {
|
||||
display: none;
|
||||
min-height: 400px;
|
||||
overflow: auto;
|
||||
border-top: 0.1rem solid #d1d1d1;
|
||||
border-bottom: 0.1rem solid #d1d1d1;
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
display: inline-block;
|
||||
.review .fyi {
|
||||
margin-bottom: 15px;
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.pagination .button {
|
||||
padding: 0 5px;
|
||||
height: 2rem;
|
||||
.review {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.pagination .page-index {
|
||||
font-size: small;
|
||||
.review .delete-preview .markdownx-preview {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin: 20px 0;
|
||||
}
|
89
common/static/css/boofilsic_modal.css
Normal file
89
common/static/css/boofilsic_modal.css
Normal file
|
@ -0,0 +1,89 @@
|
|||
.bg-mask {
|
||||
background-color: black;
|
||||
z-index: 1;
|
||||
filter: opacity(20%);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: none;
|
||||
}
|
||||
.modal {
|
||||
z-index: 2;
|
||||
display: none;
|
||||
position: fixed;
|
||||
width: 500px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
background-color: var(--bright);
|
||||
/* border: solid 2px var(--light); */
|
||||
padding: 20px 20px 0px 20px;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
/* border-bottom: 1px solid var(--light); */
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.modal-head .modal-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-head .modal-close svg {
|
||||
height: 10px;
|
||||
cursor: pointer;
|
||||
fill: var(--light);
|
||||
}
|
||||
|
||||
.modal-body form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-body, .modal-body label {
|
||||
font-size: small;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.modal-body .modal-selection ul {
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.modal-body .modal-selection ul li, .modal-body .modal-selection ul label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.modal-body .modal-selection ul li {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.modal-body .modal-checkbox {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.modal-body .modal-option {
|
||||
|
||||
}
|
||||
|
||||
.modal-body .modal-button {
|
||||
margin: 15px 0 15px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-body textarea {
|
||||
height: 160px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.modal-body input[type="checkbox"], .modal-body input[type="radio"] {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
$(document).ready( function() {
|
||||
// assume there is only one input[file] on page
|
||||
$("input[type='file']").each(function() {
|
||||
$(this).after('<img src="#" alt="" id="previewImage" style="margin:10px 0;"/>');
|
||||
$(this).after('<img src="#" alt="" id="previewImage" style="margin:10px 0; max-width:500px;"/>');
|
||||
})
|
||||
|
||||
// mark required
|
||||
|
|
25
common/static/js/create_update_review.js
Normal file
25
common/static/js/create_update_review.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
$(document).ready( function() {
|
||||
|
||||
$(".markdownx textarea").attr("placeholder", "拖拽图片至编辑框即可插入哦~");
|
||||
|
||||
$(".preview-button").click(function() {
|
||||
if ($(".markdownx-preview").is(":visible")) {
|
||||
$(".preview-button").text("预览");
|
||||
$(".markdownx-preview").hide();
|
||||
$(".markdownx textarea").show();
|
||||
} else {
|
||||
$(".preview-button").text("编辑");
|
||||
$(".markdownx-preview").show();
|
||||
$(".markdownx textarea").hide();
|
||||
}
|
||||
});
|
||||
|
||||
let ratingLabels = $("#main .rating-star");
|
||||
$(ratingLabels).each( function(index, value) {
|
||||
let ratingScore = $(this).data("rating-score") / 2;
|
||||
$(this).starRating({
|
||||
initialRating: ratingScore,
|
||||
readOnly: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,12 +1,104 @@
|
|||
$(document).ready( function() {
|
||||
// readonly star rating
|
||||
let ratingLabels = $(".rating-star");
|
||||
|
||||
|
||||
$(".modal-close").on('click', function() {
|
||||
$(this).parents(".modal").hide();
|
||||
$(".bg-mask").hide();
|
||||
});
|
||||
|
||||
$("#aside .mark .button-group .button").each(function() {
|
||||
$(this).click(function(e) {
|
||||
e.preventDefault();
|
||||
let title = $(this).text().trim();
|
||||
$(".mark-modal .modal-title").text(title);
|
||||
$(".mark-modal .modal-body textarea").val("");
|
||||
let status = $(this).data('status')
|
||||
$("input[name='status'][value='"+status+"']").prop("checked", true)
|
||||
$(".bg-mask").show();
|
||||
$(".mark-modal").show();
|
||||
|
||||
// if wish, hide rating widget in modal
|
||||
if ($(this).attr("id") == "wishButton") {
|
||||
console.log($(this).attr("id"))
|
||||
$(".mark-modal .rating-star-edit").hide();
|
||||
} else {
|
||||
$(".mark-modal .rating-star-edit").show();
|
||||
}
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
$(".mark a.edit").click(function(e) {
|
||||
e.preventDefault();
|
||||
let title = $(".mark-status-label").text().trim();
|
||||
$(".mark-modal .modal-title").text(title);
|
||||
$(".bg-mask").show();
|
||||
$(".mark-modal").show();
|
||||
});
|
||||
|
||||
// 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: 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,
|
||||
});
|
||||
});
|
||||
|
||||
// editable rating star in modal
|
||||
ratingLabels = $("#modals .rating-star-edit");
|
||||
$(ratingLabels).each( function(index, value) {
|
||||
let ratingScore = $("input[type='hidden'][name='rating']").val() / 2;
|
||||
let label = $(this);
|
||||
label.starRating({
|
||||
initialRating: ratingScore,
|
||||
starSize: 20,
|
||||
onHover: function(currentIndex, currentRating, $el){
|
||||
$("input[type='hidden'][name='rating']").val(currentIndex);
|
||||
},
|
||||
onLeave: function(currentIndex, currentRating, $el){
|
||||
$("input[type='hidden'][name='rating']").val(currentRating * 2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// hide rating star when select wish
|
||||
const WISH_CODE = 1;
|
||||
if ($(".modal-selection input[type='radio']:checked").val() == WISH_CODE) {
|
||||
$(".mark-modal .rating-star-edit").hide();
|
||||
}
|
||||
$(".modal-selection input[type='radio']").click(function() {
|
||||
// 2 is the status code of wish
|
||||
if ($(this).val() == WISH_CODE) {
|
||||
$(".mark-modal .rating-star-edit").hide();
|
||||
} else {
|
||||
$(".mark-modal .rating-star-edit").show();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// show confirm modal
|
||||
$(".mark form a").click(function(e) {
|
||||
e.preventDefault();
|
||||
$(".modal.confirm-modal").show();
|
||||
$(".bg-mask").show();
|
||||
});
|
||||
|
||||
// confirm modal
|
||||
$(".confirm-modal input[type='submit']").click(function(e) {
|
||||
e.preventDefault();
|
||||
$(".mark form").submit();
|
||||
});
|
||||
|
||||
});
|
|
@ -4,6 +4,12 @@ $(document).ready( function() {
|
|||
let mast_uri = $("#mastodonURI").text();
|
||||
let id = $("#userMastodonID").text();
|
||||
|
||||
let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let followersSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let followingSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
$("#userInfoCard").append(userInfoSpinner);
|
||||
$("#userRelationCard h5:first").append(followingSpinner);
|
||||
$("#userRelationCard h5:last").append(followersSpinner);
|
||||
$(".mast-following-more").hide();
|
||||
$(".mast-followers-more").hide();
|
||||
|
||||
|
@ -21,6 +27,7 @@ $(document).ready( function() {
|
|||
$(".mast-user .mast-avatar").attr("src", userData.avatar);
|
||||
$(".mast-user .mast-displayname").html(userName);
|
||||
$(".mast-user .mast-brief").text($(userData.note).text());
|
||||
$(userInfoSpinner).remove();
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -28,7 +35,7 @@ $(document).ready( function() {
|
|||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userList) {
|
||||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-followers").hide();
|
||||
} else {
|
||||
|
@ -42,13 +49,16 @@ $(document).ready( function() {
|
|||
temp = $(template).clone();
|
||||
temp.find("img").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find("a").text(data.display_name);
|
||||
temp.find("a").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find("a").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id) + "?is_mastodon_id=true";
|
||||
temp.find("a").attr('href', url);
|
||||
$(".mast-followers").append(temp);
|
||||
});
|
||||
}
|
||||
$(followersSpinner).remove();
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -56,7 +66,7 @@ $(document).ready( function() {
|
|||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userList) {
|
||||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-following").hide();
|
||||
} else {
|
||||
|
@ -70,13 +80,17 @@ $(document).ready( function() {
|
|||
temp = $(template).clone()
|
||||
temp.find("img").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find("a").text(data.display_name);
|
||||
temp.find("a").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find("a").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id) + "?is_mastodon_id=true";
|
||||
temp.find("a").attr('href', url);
|
||||
$(".mast-following").append(temp);
|
||||
});
|
||||
}
|
||||
$(followingSpinner).remove();
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@ const API_FOLLOWERS = "/api/v1/accounts/:id/followers"
|
|||
// GET
|
||||
const API_FOLLOWING = "/api/v1/accounts/:id/following"
|
||||
// GET
|
||||
const API_ACCOUNT = '/api/v1/accounts/:id'
|
||||
const API_GET_ACCOUNT = '/api/v1/accounts/:id'
|
||||
|
||||
const NUMBER_PER_REQUEST = 20
|
||||
|
||||
|
||||
// [
|
||||
|
@ -60,8 +62,11 @@ function getFollowers(id, mastodonURI, token, callback) {
|
|||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
},
|
||||
success: function(data){
|
||||
callback(data);
|
||||
data: {
|
||||
'limit': NUMBER_PER_REQUEST
|
||||
},
|
||||
success: function(data, status, request){
|
||||
callback(data, request);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -74,8 +79,11 @@ function getFollowing(id, mastodonURI, token, callback) {
|
|||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
},
|
||||
success: function(data){
|
||||
callback(data);
|
||||
data: {
|
||||
'limit': NUMBER_PER_REQUEST
|
||||
},
|
||||
success: function(data, status, request){
|
||||
callback(data, request);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -113,7 +121,7 @@ function getFollowing(id, mastodonURI, token, callback) {
|
|||
// ]
|
||||
// }
|
||||
function getUserInfo(id, mastodonURI, token, callback) {
|
||||
let url = mastodonURI + API_ACCOUNT.replace(":id", id);
|
||||
let url = mastodonURI + API_GET_ACCOUNT.replace(":id", id);
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
|
|
|
@ -26,6 +26,10 @@ body {
|
|||
line-height: 1.6;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.3rem solid #d1d1d1;
|
||||
margin-left: 0;
|
||||
|
@ -599,4 +603,3 @@ img {
|
|||
float: right;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=milligram.css.map */
|
30
common/templates/common/error.html
Normal file
30
common/templates/common/error.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="refresh" content="3;url={% url 'common:home' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
|
||||
<title>{% trans '错误' %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="logo" class="logo">
|
||||
</a>
|
||||
<div class="main-msg">
|
||||
{{ msg }}
|
||||
</div>
|
||||
<div class="sec-msg">
|
||||
{% if secondary_msg %}
|
||||
{{ secondary_msg }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -30,7 +30,9 @@
|
|||
<input type="search" class="search-box" name="keywords" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if request.user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -41,19 +43,19 @@
|
|||
|
||||
<div class="set" id="bookWish">
|
||||
<h5 class="set-title">
|
||||
{% trans '我想看的书' %}
|
||||
{% trans '想看的书' %}
|
||||
</h5>
|
||||
{% if wish_books_more %}
|
||||
<a href="" class="more-link">{% trans '更多' %}</a>
|
||||
<a href="{% url 'users:book_list' user.id 'wish' %}" class="more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="row set-item-list">
|
||||
{% for wish_book in wish_books %}
|
||||
{% for wish_book_mark in wish_book_marks %}
|
||||
<li class="column column-20 set-item">
|
||||
<!-- <img src="{{ wish_book.cover.url }}" alt="{{ wish_book.title }}"> -->
|
||||
<a href="{% url 'books:retrieve' wish_book.id %}" >
|
||||
<img src="{% static 'img/default.jpg' %}" alt="" class="set-item-image">
|
||||
<span class="set-item-title">{{ wish_book.title | truncate:9 }}</span>
|
||||
|
||||
<a href="{% url 'books:retrieve' wish_book_mark.book.id %}" >
|
||||
<img src="{{ wish_book_mark.book.cover.url }}" alt="" class="set-item-image">
|
||||
<span class="set-item-title">{{ wish_book_mark.book.title | truncate:15 }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
|
@ -63,19 +65,19 @@
|
|||
</div>
|
||||
<div class="set" id="bookDo">
|
||||
<h5 class="set-title">
|
||||
{% trans '我在看的书' %}
|
||||
{% trans '在看的书' %}
|
||||
</h5>
|
||||
{% if do_books_more %}
|
||||
<a href="" class="more-link">{% trans '更多' %}</a>
|
||||
<a href="{% url 'users:book_list' user.id 'do' %}" class="more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="row set-item-list">
|
||||
{% for do_book in do_books %}
|
||||
{% for do_book_mark in do_book_marks %}
|
||||
<li class="column column-20 set-item">
|
||||
<!-- <img src="{{ do_book.cover.url }}" alt="{{ do_book.title }}"> -->
|
||||
<a href="{% url 'books:retrieve' do_book.id %}" >
|
||||
<img src="{% static 'img/default.jpg' %}" alt="" class="set-item-image">
|
||||
<span class="set-item-title">{{ do_book.title | truncate:9 }}</span>
|
||||
|
||||
<a href="{% url 'books:retrieve' do_book_mark.book.id %}" >
|
||||
<img src="{{ do_book_mark.book.cover.url }}" alt="" class="set-item-image">
|
||||
<span class="set-item-title">{{ do_book_mark.book.title | truncate:15 }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
|
@ -85,19 +87,19 @@
|
|||
</div>
|
||||
<div class="set" id="bookCollect">
|
||||
<h5 class="set-title">
|
||||
{% trans '我看过的书' %}
|
||||
{% trans '看过的书' %}
|
||||
</h5>
|
||||
{% if collect_books_more %}
|
||||
<a href="" class="more-link">{% trans '更多' %}</a>
|
||||
<a href="{% url 'users:book_list' user.id 'collect' %}" class="more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="row set-item-list">
|
||||
{% for collect_book in collect_books %}
|
||||
{% for collect_book_mark in collect_book_marks %}
|
||||
<li class="column column-20 set-item">
|
||||
<!-- <img src="{{ collect_book.cover.url }}" alt="{{ collect_book.title }}"> -->
|
||||
<a href="{% url 'books:retrieve' collect_book.id %}" >
|
||||
<img src="{% static 'img/default.jpg' %}" alt="" class="set-item-image">
|
||||
<span class="set-item-title">{{ collect_book.title | truncate:9 }}</span>
|
||||
|
||||
<a href="{% url 'books:retrieve' collect_book_mark.book.id %}" >
|
||||
<img src="{{ collect_book_mark.book.cover.url }}" alt="" class="set-item-image">
|
||||
<span class="set-item-title">{{ collect_book_mark.book.title | truncate:15 }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
|
@ -111,21 +113,23 @@
|
|||
<div class="aside-card mast-user" id="userInfoCard">
|
||||
<div class="clearfix">
|
||||
<img src="" class="info-avatar mast-avatar" alt="{{ user.username }}">
|
||||
<a href="">
|
||||
<a href="{% url 'users:home' user.id %}">
|
||||
<h5 class="info-name mast-displayname"></h5>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="info-brief mast-brief"></p>
|
||||
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||
<!-- <a href="#" class="report">{% trans '举报用户' %}</a> -->
|
||||
|
||||
{% if request.user != user %}
|
||||
<a href="{% url 'users:report' %}" class="report">{% trans '举报用户' %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="relation-card" id="userRelationCard">
|
||||
<h5 class="relation-label">
|
||||
{% trans '我关注的人' %}
|
||||
{% trans '关注的人' %}
|
||||
</h5>
|
||||
<a href="" class="more-link mast-following-more">{% trans '更多' %}</a>
|
||||
<a href="{% url 'users:following' user.id %}" class="more-link mast-following-more">{% trans '更多' %}</a>
|
||||
<ul class="row mast-following relation-user-list">
|
||||
<li class="column column-25 relation-user">
|
||||
<img src="" alt="" class="relation-avatar">
|
||||
|
@ -133,9 +137,9 @@
|
|||
</li>
|
||||
</ul>
|
||||
<h5 class="relation-label">
|
||||
{% trans '关注我的人' %}
|
||||
{% trans '被他们关注' %}
|
||||
</h5>
|
||||
<a href="" class="more-link mast-followers-more">{% trans '更多' %}</a>
|
||||
<a href="{% url 'users:followers' user.id %}" class="more-link mast-followers-more">{% trans '更多' %}</a>
|
||||
<ul class="row mast-followers relation-user-list">
|
||||
<li class="column column-25 relation-user">
|
||||
<img src="" alt="" class="relation-avatar">
|
||||
|
@ -144,7 +148,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
{% if user.is_staff %}
|
||||
{% if request.user.is_staff %}
|
||||
<div class="report-card" id="reportMessageCard">
|
||||
<h5 class="report-label">{% trans '举报信息' %}</h5>
|
||||
<ul class="report-list">
|
||||
|
@ -155,6 +159,7 @@
|
|||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
<a href="{% url 'users:manage_report' %}">全部举报</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -172,6 +177,23 @@
|
|||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
<div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
|
||||
<div class="spinner" id="spinner" hidden>
|
||||
<div class="lds-spinner">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<title>{% trans 'Boofilsic - 搜索结果' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/search_result.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
|
@ -33,7 +33,9 @@
|
|||
value="{% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -46,10 +48,12 @@
|
|||
{% for book in items %}
|
||||
|
||||
<li class="result-item clearfix">
|
||||
<img src="{% static 'img/default.jpg' %}" alt="" class="result-book-cover">
|
||||
<a href="{% url 'books:retrieve' book.id %}">
|
||||
<img src="{{ book.cover.url }}" alt="" class="result-book-cover">
|
||||
</a>
|
||||
<div class="result-info">
|
||||
|
||||
<a href="{% url 'books:retrieve' book.id %}" class="result-book-title">
|
||||
<a href="{% url 'books:retrieve' book.id %}" class="result-item-title">
|
||||
{% if request.GET.keywords %}
|
||||
{{ book.title | highlight:request.GET.keywords }}
|
||||
{% else %}
|
||||
|
@ -92,7 +96,7 @@
|
|||
{{ book.orig_title }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<p class="result-book-brief">
|
||||
<p class="result-item-brief">
|
||||
{{ book.brief | truncate:170 }}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -150,12 +154,12 @@
|
|||
|
||||
<div id="aside">
|
||||
<div class="aside-card">
|
||||
|
||||
<div>
|
||||
|
||||
{% trans '没有想要的结果?' %}
|
||||
<div class="add-nav">
|
||||
<div>{% trans '没有想要的结果?' %}</div>
|
||||
<a href="{% url 'books:create' %}" class="button add-button">{% trans '添加一个条目' %}</a>
|
||||
<div>{% trans '或者' %}</div>
|
||||
<a href="{% url 'books:scrape' %}{% if request.GET.keywords %}?keywords={{ request.GET.keywords }}{% endif %}" class="button add-button">{% trans '从表瓣剽取数据d(≖ ◡ ≖)✧' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'books:create' %}" class="button add-button">{% trans '添加一个条目' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -8,7 +8,15 @@
|
|||
margin-left: 1%;
|
||||
}
|
||||
</style>
|
||||
<div class="widget-value-key-input"></div>
|
||||
<div class="widget-value-key-input">
|
||||
{% if widget.value != None %}
|
||||
|
||||
{% for k, v in widget.keyvalue_pairs.items %}
|
||||
<input type="text" value="{{ k }}" ><input type="text" value="{{ v }}">
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="text" class="widget-value-key-input-data" hidden name="{{ widget.name }}">
|
||||
<script>
|
||||
//init
|
||||
|
|
|
@ -18,14 +18,15 @@ ITEMS_PER_PAGE = 20
|
|||
@login_required
|
||||
def home(request):
|
||||
if request.method == 'GET':
|
||||
books = Book.objects.filter(book_marks__owner=request.user)
|
||||
|
||||
do_books = books.filter(book_marks__status=MarkStatusEnum.DO)
|
||||
do_books_more = True if do_books.count() > BOOKS_PER_SET else False
|
||||
wish_books = books.filter(book_marks__status=MarkStatusEnum.WISH)
|
||||
wish_books_more = True if wish_books.count() > BOOKS_PER_SET else False
|
||||
collect_books = books.filter(book_marks__status=MarkStatusEnum.COLLECT)
|
||||
collect_books_more = True if collect_books.count() > BOOKS_PER_SET else False
|
||||
do_book_marks = request.user.user_bookmarks.filter(status=MarkStatusEnum.DO)
|
||||
do_books_more = True if do_book_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
wish_book_marks = request.user.user_bookmarks.filter(status=MarkStatusEnum.WISH)
|
||||
wish_books_more = True if wish_book_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
collect_book_marks = request.user.user_bookmarks.filter(status=MarkStatusEnum.COLLECT)
|
||||
collect_books_more = True if collect_book_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
reports = Report.objects.order_by('-submitted_time').filter(is_read=False)
|
||||
# reports = Report.objects.latest('submitted_time').filter(is_read=False)
|
||||
|
@ -34,9 +35,9 @@ def home(request):
|
|||
request,
|
||||
'common/home.html',
|
||||
{
|
||||
'do_books': do_books[:BOOKS_PER_SET],
|
||||
'wish_books': wish_books[:BOOKS_PER_SET],
|
||||
'collect_books': collect_books[:BOOKS_PER_SET],
|
||||
'do_book_marks': do_book_marks[:BOOKS_PER_SET],
|
||||
'wish_book_marks': wish_book_marks[:BOOKS_PER_SET],
|
||||
'collect_book_marks': collect_book_marks[:BOOKS_PER_SET],
|
||||
'do_books_more': do_books_more,
|
||||
'wish_books_more': wish_books_more,
|
||||
'collect_books_more': collect_books_more,
|
||||
|
@ -47,6 +48,7 @@ def home(request):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def search(request):
|
||||
if request.method == 'GET':
|
||||
# in the future when more modules are added...
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from .models import *
|
||||
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(Report)
|
||||
admin.site.register(User)
|
|
@ -1,88 +0,0 @@
|
|||
import requests
|
||||
from django.shortcuts import reverse
|
||||
from django.contrib.auth.backends import ModelBackend, UserModel
|
||||
from boofilsic.settings import MASTODON_DOMAIN_NAME, CLIENT_ID, CLIENT_SECRET
|
||||
from common.mastodon.api import *
|
||||
|
||||
|
||||
class OAuth2Backend(ModelBackend):
|
||||
""" Used to glue OAuth2 and Django User model """
|
||||
# "authenticate() should check the credentials it gets and returns
|
||||
# a user object that matches those credentials."
|
||||
# arg request is an interface specification, not used in this implementation
|
||||
def authenticate(self, request, token=None, username=None, **kwargs):
|
||||
""" when username is provided, assume that token is newly obtained and valid """
|
||||
if token is None:
|
||||
return
|
||||
|
||||
if username is None:
|
||||
user_data = get_user_data(token)
|
||||
if user_data:
|
||||
username = user_data['username']
|
||||
else:
|
||||
# aquiring user data fail means token is invalid thus auth fail
|
||||
return None
|
||||
|
||||
# when username is provided, assume that token is newly obtained and valid
|
||||
try:
|
||||
user = UserModel._default_manager.get_by_natural_key(user_data['username'])
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
else:
|
||||
if self.user_can_authenticate(user):
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
def obtain_token(request, code):
|
||||
""" Returns token if success else None. """
|
||||
payload = {
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'redirect_uri': f"http://{request.get_host()}{reverse('users:OAuth2_login')}",
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'scope': 'read write'
|
||||
}
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + OAUTH_TOKEN
|
||||
response = requests.post(url, data=payload)
|
||||
if response.status_code != 200:
|
||||
return
|
||||
data = response.json()
|
||||
return data.get('access_token')
|
||||
|
||||
|
||||
def get_user_data(token):
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + VERIFY_ACCOUNT_CREDENTIALS
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def revoke_token(token):
|
||||
payload = {
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
'scope': token
|
||||
}
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + REVOKE_TOKEN
|
||||
response = requests.post(url, data=payload)
|
||||
|
||||
|
||||
def verify_token(token):
|
||||
""" Check if the token is valid and is of local instance. """
|
||||
url = 'https://' + MASTODON_DOMAIN_NAME + VERIFY_ACCOUNT_CREDENTIALS
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
res_data = response.json()
|
||||
# check if is local instance user
|
||||
if res_data['acct'] == res_data['username']:
|
||||
return True
|
||||
return False
|
16
users/forms.py
Normal file
16
users/forms.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django import forms
|
||||
from .models import Report
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ReportForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Report
|
||||
fields = [
|
||||
'reported_user',
|
||||
'image',
|
||||
'message',
|
||||
]
|
||||
widgets = {
|
||||
'message': forms.Textarea(attrs={'placeholder': _("详情")}),
|
||||
}
|
|
@ -1,21 +1,23 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from boofilsic.settings import REPORT_MEDIA_PATH_ROOT, DEFAULT_PASSWORD
|
||||
|
||||
|
||||
def report_image_path(instance, filename):
|
||||
raise NotImplementedError("UUID!!!!!!!!!!!")
|
||||
ext = filename.split('.')[-1]
|
||||
filename = "%s.%s" % (uuid.uuid4(), ext)
|
||||
root = ''
|
||||
if REPORT_MEDIA_PATH_ROOT.endswith('/'):
|
||||
root = REPORT_MEDIA_PATH_ROOT
|
||||
else:
|
||||
root = REPORT_MEDIA_PATH_ROOT + '/'
|
||||
return root + datetime.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
mastodon_id = models.IntegerField()
|
||||
mastodon_id = models.IntegerField(unique=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Automatically populate password field with DEFAULT_PASSWORD before saving."""
|
||||
|
@ -29,6 +31,7 @@ class Report(models.Model):
|
|||
image = models.ImageField(upload_to=report_image_path, height_field=None, width_field=None, max_length=None, blank=True, default='')
|
||||
is_read = models.BooleanField(default=False)
|
||||
submitted_time = models.DateTimeField(auto_now_add=True)
|
||||
message = models.CharField(max_length=1000)
|
||||
|
||||
|
||||
|
||||
|
|
187
users/static/js/followers_list.js
Normal file
187
users/static/js/followers_list.js
Normal file
|
@ -0,0 +1,187 @@
|
|||
|
||||
$(document).ready( function() {
|
||||
let token = $("#oauth2Token").text();
|
||||
let mast_uri = $("#mastodonURI").text();
|
||||
let id = $("#userMastodonID").text();
|
||||
let nextUrl = null;
|
||||
let requesting = false;
|
||||
|
||||
let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let followersSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let followingSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let mainSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
|
||||
$("#main .user:first").hide();
|
||||
|
||||
$("#main").append(mainSpinner);
|
||||
$("#userInfoCard").append(userInfoSpinner);
|
||||
$("#userRelationCard h5:first").append(followingSpinner);
|
||||
$("#userRelationCard h5:last").append(followersSpinner);
|
||||
$(".mast-following-more").hide();
|
||||
$(".mast-followers-more").hide();
|
||||
|
||||
getUserInfo(
|
||||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userData) {
|
||||
let userName;
|
||||
if (userData.display_name) {
|
||||
userName = translateEmojis(userData.display_name, userData.emojis);
|
||||
} else {
|
||||
userName = userData.username;
|
||||
}
|
||||
$(".mast-user .mast-avatar").attr("src", userData.avatar);
|
||||
$(".mast-user .mast-displayname").html(userName);
|
||||
$(".mast-user .mast-brief").text($(userData.note).text());
|
||||
$(userInfoSpinner).remove();
|
||||
}
|
||||
);
|
||||
|
||||
getFollowers(
|
||||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-followers").hide();
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
userList = userList.slice(0, 4);
|
||||
$(".mast-followers-more").show();
|
||||
}
|
||||
let template = $(".mast-followers li").clone();
|
||||
$(".mast-followers").html("");
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone();
|
||||
temp.find("img").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find("a").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find("a").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
$(".mast-followers").append(temp);
|
||||
});
|
||||
}
|
||||
$(followersSpinner).remove();
|
||||
// main
|
||||
let template = $("#main .user").clone().show();
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone()
|
||||
temp.find(".avatar").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find(".user-name").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find(".user-name").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
temp.find(".user-brief").text($(data.note).text());
|
||||
$("#main .user:last").after(temp);
|
||||
});
|
||||
|
||||
mainSpinner.hide();
|
||||
request.getResponseHeader('link').split(',').forEach(link => {
|
||||
if (link.includes('next')) {
|
||||
let regex = /<(.*?)>/;
|
||||
nextUrl = link.match(regex)[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
getFollowing(
|
||||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userList, request) {
|
||||
// aside
|
||||
if (userList.length == 0) {
|
||||
$("#aside .mast-following").hide();
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
userList = userList.slice(0, 4);
|
||||
$("#aside .mast-following-more").show();
|
||||
}
|
||||
let template = $("#aside .mast-following li").clone();
|
||||
$("#aside .mast-following").html("");
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone()
|
||||
temp.find("img").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find("a").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find("a").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
$("#aside .mast-following").append(temp);
|
||||
});
|
||||
}
|
||||
$(followingSpinner).remove();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
$(window).scroll(function() {
|
||||
let scrollPosition = $(window).scrollTop();
|
||||
// test if scoll to bottom
|
||||
if (scrollPosition + 0.5> $(document).height()-$(window).height()) {
|
||||
if (!requesting && nextUrl) {
|
||||
// acquire lock
|
||||
requesting = true;
|
||||
mainSpinner.show();
|
||||
$.ajax({
|
||||
url: nextUrl,
|
||||
method: 'GET',
|
||||
data: {
|
||||
'limit': NUMBER_PER_REQUEST,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
},
|
||||
success: function(userList, status, request){
|
||||
if(userList.length == 0 ) {
|
||||
mainSpinner.hide();
|
||||
return;
|
||||
}
|
||||
let template = $("#main .user:first").clone();
|
||||
let newUrlFlag = false;
|
||||
request.getResponseHeader('link').split(',').forEach(link => {
|
||||
if (link.includes('next')) {
|
||||
let regex = /<(.*?)>/;
|
||||
nextUrl = link.match(regex)[1];
|
||||
newUrlFlag = true;
|
||||
}
|
||||
});
|
||||
if (!newUrlFlag) {
|
||||
nextUrl = null;
|
||||
}
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone()
|
||||
temp.find(".avatar").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find(".user-name").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find(".user-name").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
temp.find(".user-brief").text($(data.note).text());
|
||||
$("#main .user:last").after(temp);
|
||||
});
|
||||
mainSpinner.hide();
|
||||
// release lock
|
||||
// console.log(userList[userList.length-1].username)
|
||||
// console.log(nextUrl)
|
||||
requesting = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
188
users/static/js/following_list.js
Normal file
188
users/static/js/following_list.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
|
||||
$(document).ready( function() {
|
||||
let token = $("#oauth2Token").text();
|
||||
let mast_uri = $("#mastodonURI").text();
|
||||
let id = $("#userMastodonID").text();
|
||||
let nextUrl = null;
|
||||
let requesting = false;
|
||||
|
||||
let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let followersSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let followingSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
let mainSpinner = $("#spinner").clone().removeAttr("hidden");
|
||||
|
||||
$("#main .user:first").hide();
|
||||
|
||||
$("#main").append(mainSpinner);
|
||||
$("#userInfoCard").append(userInfoSpinner);
|
||||
$("#userRelationCard h5:first").append(followingSpinner);
|
||||
$("#userRelationCard h5:last").append(followersSpinner);
|
||||
$(".mast-following-more").hide();
|
||||
$(".mast-followers-more").hide();
|
||||
|
||||
getUserInfo(
|
||||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userData) {
|
||||
let userName;
|
||||
if (userData.display_name) {
|
||||
userName = translateEmojis(userData.display_name, userData.emojis);
|
||||
} else {
|
||||
userName = userData.username;
|
||||
}
|
||||
$(".mast-user .mast-avatar").attr("src", userData.avatar);
|
||||
$(".mast-user .mast-displayname").html(userName);
|
||||
$(".mast-user .mast-brief").text($(userData.note).text());
|
||||
$(userInfoSpinner).remove();
|
||||
}
|
||||
);
|
||||
|
||||
getFollowers(
|
||||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userList, request) {
|
||||
if (userList.length == 0) {
|
||||
$(".mast-followers").hide();
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
userList = userList.slice(0, 4);
|
||||
$(".mast-followers-more").show();
|
||||
}
|
||||
let template = $(".mast-followers li").clone();
|
||||
$(".mast-followers").html("");
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone();
|
||||
temp.find("img").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find("a").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find("a").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
$(".mast-followers").append(temp);
|
||||
});
|
||||
}
|
||||
$(followersSpinner).remove();
|
||||
}
|
||||
);
|
||||
|
||||
getFollowing(
|
||||
id,
|
||||
mast_uri,
|
||||
token,
|
||||
function(userList, request) {
|
||||
// aside
|
||||
if (userList.length == 0) {
|
||||
$("#aside .mast-following").hide();
|
||||
} else {
|
||||
if (userList.length > 4){
|
||||
userList = userList.slice(0, 4);
|
||||
$("#aside .mast-following-more").show();
|
||||
}
|
||||
let template = $("#aside .mast-following li").clone();
|
||||
$("#aside .mast-following").html("");
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone()
|
||||
temp.find("img").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find("a").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find("a").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
$("#aside .mast-following").append(temp);
|
||||
});
|
||||
}
|
||||
$(followingSpinner).remove();
|
||||
|
||||
// main
|
||||
let template = $("#main .user").clone().show();
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone()
|
||||
temp.find(".avatar").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find(".user-name").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find(".user-name").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
temp.find(".user-brief").text($(data.note).text());
|
||||
$("#main .user:last").after(temp);
|
||||
});
|
||||
|
||||
mainSpinner.hide();
|
||||
request.getResponseHeader('link').split(',').forEach(link => {
|
||||
if (link.includes('next')) {
|
||||
let regex = /<(.*?)>/;
|
||||
nextUrl = link.match(regex)[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
$(window).scroll(function() {
|
||||
let scrollPosition = $(window).scrollTop();
|
||||
// test if scoll to bottom
|
||||
if (scrollPosition + 0.5> $(document).height()-$(window).height()) {
|
||||
if (!requesting && nextUrl) {
|
||||
// acquire lock
|
||||
requesting = true;
|
||||
mainSpinner.show();
|
||||
$.ajax({
|
||||
url: nextUrl,
|
||||
method: 'GET',
|
||||
data: {
|
||||
'limit': NUMBER_PER_REQUEST,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
},
|
||||
success: function(userList, status, request){
|
||||
if(userList.length == 0 ) {
|
||||
mainSpinner.hide();
|
||||
return;
|
||||
}
|
||||
let template = $("#main .user:first").clone();
|
||||
let newUrlFlag = false;
|
||||
request.getResponseHeader('link').split(',').forEach(link => {
|
||||
if (link.includes('next')) {
|
||||
let regex = /<(.*?)>/;
|
||||
nextUrl = link.match(regex)[1];
|
||||
newUrlFlag = true;
|
||||
}
|
||||
});
|
||||
if (!newUrlFlag) {
|
||||
nextUrl = null;
|
||||
}
|
||||
userList.forEach(data => {
|
||||
temp = $(template).clone()
|
||||
temp.find(".avatar").attr("src", data.avatar);
|
||||
if (data.display_name) {
|
||||
temp.find(".user-name").html(translateEmojis(data.display_name, data.emojis));
|
||||
} else {
|
||||
temp.find(".user-name").text(data.username);
|
||||
}
|
||||
let url = $("#userPageURL").text().replace('0', data.id);
|
||||
temp.find("a").attr('href', url);
|
||||
temp.find(".user-brief").text($(data.note).text());
|
||||
$("#main .user:last").after(temp);
|
||||
});
|
||||
mainSpinner.hide();
|
||||
// release lock
|
||||
// console.log(userList[userList.length-1].username)
|
||||
// console.log(nextUrl)
|
||||
requesting = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
234
users/templates/users/list.html
Normal file
234
users/templates/users/list.html
Normal file
|
@ -0,0 +1,234 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 书' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<script src="{% static 'js/mastodon.js' %}"></script>
|
||||
<script src="{% static 'js/home.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<input type="search" class="search-box" name="keywords"
|
||||
value="{% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if request.user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<ul class="result-items">
|
||||
|
||||
{% for mark in marks %}
|
||||
|
||||
<li class="result-item clearfix">
|
||||
<a href="{% url 'books:retrieve' mark.book.id %}">
|
||||
<img src="{{ mark.book.cover.url }}" alt="" class="result-book-cover">
|
||||
</a>
|
||||
<div class="result-info">
|
||||
|
||||
<a href="{% url 'books:retrieve' mark.book.id %}" class="result-item-title">
|
||||
{{ mark.book.title }}
|
||||
</a>
|
||||
{% if mark.book.rating %}
|
||||
|
||||
<div class="rating-star" data-rating-score="{{ mark.book.rating | floatformat:"0" }}"></div>
|
||||
<span class="rating-score">
|
||||
{{ mark.book.rating }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="rating-empty"> {% trans '暂无评分' %}</span>
|
||||
{% endif %}
|
||||
<span class="result-book-info">
|
||||
{% if mark.book.pub_year %}
|
||||
{{ mark.book.pub_year }}{% trans '年' %} /
|
||||
{% if mark.book.pub_month %}
|
||||
{{ mark.book.pub_month }}{% trans '月' %} /
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if mark.book.author %}
|
||||
{% trans '作者' %}
|
||||
{% for author in mark.book.author %}
|
||||
{{ author }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}/
|
||||
{% endif %}
|
||||
|
||||
{% if mark.book.translator %}
|
||||
{% trans '译者' %}
|
||||
{% for translator in mark.book.translator %}
|
||||
{{ translator }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}/
|
||||
{% endif %}
|
||||
|
||||
{% if mark.book.orig_title %}
|
||||
{% trans '原名' %}
|
||||
{{ mark.book.orig_title }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<p class="result-item-brief">
|
||||
{{ mark.book.brief | truncate:170 }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
{% trans '无结果' %}
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
<div class="pagination" >
|
||||
|
||||
<a
|
||||
{% if marks.has_previous %}
|
||||
href="?page=1"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not marks.has_previous %}disabled{% endif %} class="button button-clear">{% trans "首页" %}</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
{% if marks.has_previous %}
|
||||
href="?page={{ marks.previous_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not marks.has_previous %}disabled{% endif %} class="button button-clear">{% trans "上一页" %}</button>
|
||||
</a>
|
||||
|
||||
<span class="page-index">
|
||||
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
|
||||
</span>
|
||||
|
||||
<a
|
||||
{% if marks.has_next %}
|
||||
href="?page={{ marks.next_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}
|
||||
>
|
||||
<button {% if not marks.has_next %}disabled{% endif %} class="button button-clear">{% trans "下一页" %}</button>
|
||||
</a>
|
||||
|
||||
<a
|
||||
{% if marks.has_next %}
|
||||
href="?page={{ marks.paginator.num_pages }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<button {% if not marks.has_next %}disabled{% endif %} class="button button-clear">{% trans "末页" %}</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aside">
|
||||
<div class="aside-card mast-user" id="userInfoCard">
|
||||
<div class="clearfix">
|
||||
<img src="" class="info-avatar mast-avatar" alt="{{ user.username }}">
|
||||
<a href="{% url 'users:home' user.id %}">
|
||||
<h5 class="info-name mast-displayname"></h5>
|
||||
</a>
|
||||
</div>
|
||||
<p class="info-brief mast-brief"></p>
|
||||
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||
|
||||
{% if request.user != user %}
|
||||
<a href="#" class="report">{% trans '举报用户' %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="relation-card" id="userRelationCard">
|
||||
<h5 class="relation-label">
|
||||
{% trans '关注的人' %}
|
||||
</h5>
|
||||
<a href="{% url 'users:following' user.id %}" class="more-link mast-following-more">{% trans '更多' %}</a>
|
||||
<ul class="row mast-following relation-user-list">
|
||||
<li class="column column-25 relation-user">
|
||||
<img src="" alt="" class="relation-avatar">
|
||||
<a class="relation-name"></a>
|
||||
</li>
|
||||
</ul>
|
||||
<h5 class="relation-label">
|
||||
{% trans '被他们关注' %}
|
||||
</h5>
|
||||
<a href="{% url 'users:followers' user.id %}" class="more-link mast-followers-more">{% trans '更多' %}</a>
|
||||
<ul class="row mast-followers relation-user-list">
|
||||
<li class="column column-25 relation-user">
|
||||
<img src="" alt="" class="relation-avatar">
|
||||
<a class="relation-name"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
<div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
|
||||
<div class="spinner" id="spinner" hidden>
|
||||
<div class="lds-spinner">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -1,24 +1,27 @@
|
|||
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
|
||||
<title>{% trans 'Boofilsic - 登录' %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loginBox">
|
||||
<div id="logo">
|
||||
<img src="" alt="boofilsic logo">
|
||||
</div>
|
||||
<div id="loginBox" class="box">
|
||||
|
||||
<img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
|
||||
|
||||
<div id="loginButton">
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'common:home' %}"">{% trans '前往我的主页' %}</a>
|
||||
<a href="{% url 'common:home' %}" class="button">{% trans '前往我的主页' %}</a>
|
||||
{% else %}
|
||||
<a href="{{ oauth_auth_url }}">{% trans '使用长毛象授权登录' %}</a>
|
||||
<a href="{{ oauth_auth_url }}" class="button">{% trans '使用长毛象授权登录' %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
89
users/templates/users/manage_report.html
Normal file
89
users/templates/users/manage_report.html
Normal file
|
@ -0,0 +1,89 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 管理举报' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'js/create_update.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<h4 class="nav-title">{% trans '举报信息' %}</h4>
|
||||
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
|
||||
{% for report in reports %}
|
||||
<div class="report">
|
||||
<a href="{% url 'users:home' report.submit_user.id %}">{{ report.submit_user.username }}</a>
|
||||
{% trans '举报了' %}
|
||||
<a href="{% url 'users:home' report.reported_user.id %}">{{ report.reported_user.username }}</a>
|
||||
@{{ report.submitted_time }}
|
||||
|
||||
{% if report.image %}
|
||||
<img src="{{ report.image.url }}" alt="">
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -1,16 +1,41 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register</title>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
|
||||
<title>{% trans 'Boofilsic - 注册' %}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
Are you sure you wanna join?
|
||||
<form action="{% url 'users:register' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="radio" name="confirm" id="confirmRadio">
|
||||
<button type="submit">Cut the sh*t get me in!</button>
|
||||
</form>
|
||||
<div id="loginBox" class="box">
|
||||
|
||||
<img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
|
||||
|
||||
<div id="loginButton">
|
||||
<p>欢迎来到里瓣书影音(其实现在只有书)!</p>
|
||||
<p>
|
||||
里瓣书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
|
||||
这里仍是一片处女地,丰富的内容需要大家共同创造!
|
||||
请注意虽然您可以随意发表任何言论,但试图添加垃圾数据到公共数据领域(如添加不存在的乱码的书籍)将会受到制裁!
|
||||
</p>
|
||||
<p>
|
||||
此外里瓣书影音现处于“公开阿尔法测试”阶段,您的数据存在丢失的可能,现阶段将不对用户数据负责,请您理解风险后再决定继续使用!
|
||||
</p>
|
||||
|
||||
<form action="{% url 'users:register' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="button" value="{% trans 'Cut the sh*t and get me in!' %}">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
78
users/templates/users/report.html
Normal file
78
users/templates/users/report.html
Normal file
|
@ -0,0 +1,78 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Boofilsic - 举报用户' %}</title>
|
||||
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||
<script src="{% static 'js/create_update.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
<section id="navbar" class="navbar">
|
||||
<div class="container">
|
||||
<nav class="clearfix">
|
||||
<a href="{% url 'common:home' %}">
|
||||
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||
</a>
|
||||
<h4 class="nav-title">{% trans '举报用户' %}</h4>
|
||||
|
||||
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="row">
|
||||
<div id="main">
|
||||
<form action="{% url 'users:report' %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input class="button" type="submit" value="{% trans '提交' %}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
<footer class="container">
|
||||
<a href="">whitiewhite@donotban.com</a>
|
||||
<a href="" id="githubLink">Github</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
$("#searchInput").on('keyup', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
let keywords = $(this).val();
|
||||
if (keywords)
|
||||
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -8,4 +8,10 @@ urlpatterns = [
|
|||
path('logout/', logout, name='logout'),
|
||||
path('delete/', delete, name='delete'),
|
||||
path('OAuth2_login/', OAuth2_login, name='OAuth2_login'),
|
||||
path('<int:id>/', home, name='home'),
|
||||
path('<int:id>/followers/', followers, name='followers'),
|
||||
path('<int:id>/following/', following, name='following'),
|
||||
path('<int:id>/book/<str:status>/', book_list, name='book_list'),
|
||||
path('report/', report, name='report'),
|
||||
path('manage_report/', manage_report, name='manage_report'),
|
||||
]
|
||||
|
|
263
users/views.py
263
users/views.py
|
@ -1,17 +1,27 @@
|
|||
from django.shortcuts import reverse, redirect, render
|
||||
from django.shortcuts import reverse, redirect, render, get_object_or_404
|
||||
from django.http import HttpResponseBadRequest, HttpResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth import authenticate
|
||||
from .models import User
|
||||
from .auth import *
|
||||
from boofilsic.settings import MASTODON_DOMAIN_NAME, CLIENT_ID, CLIENT_SECRET
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
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
|
||||
from common.models import MarkStatusEnum
|
||||
from books.models import *
|
||||
from boofilsic.settings import MASTODON_DOMAIN_NAME, CLIENT_ID, CLIENT_SECRET
|
||||
|
||||
|
||||
# Views
|
||||
########################################
|
||||
|
||||
# no page rendered
|
||||
@mastodon_request_included
|
||||
def OAuth2_login(request):
|
||||
""" oauth authentication and logging user into django system """
|
||||
if request.method == 'GET':
|
||||
|
@ -39,7 +49,7 @@ def OAuth2_login(request):
|
|||
def login(request):
|
||||
if request.method == 'GET':
|
||||
# TODO NOTE replace http with https!!!!
|
||||
auth_url = f"https://{MASTODON_DOMAIN_NAME}{OAUTH_AUTHORIZE}?" +\
|
||||
auth_url = f"https://{MASTODON_DOMAIN_NAME}{API_OAUTH_AUTHORIZE}?" +\
|
||||
f"client_id={CLIENT_ID}&scope=read+write&" +\
|
||||
f"redirect_uri=http://{request.get_host()}{reverse('users:OAuth2_login')}" +\
|
||||
"&response_type=code"
|
||||
|
@ -55,6 +65,8 @@ def login(request):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def logout(request):
|
||||
if request.method == 'GET':
|
||||
revoke_token(request.session['oauth_token'])
|
||||
|
@ -64,6 +76,7 @@ def logout(request):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
def register(request):
|
||||
""" register confirm page """
|
||||
if request.method == 'GET':
|
||||
|
@ -95,9 +108,247 @@ def delete(request):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def home(request, id):
|
||||
if request.method == 'GET':
|
||||
if request.GET.get('is_mastodon_id') in ['true', 'True']:
|
||||
query_kwargs = {'mastodon_id': id}
|
||||
else:
|
||||
query_kwargs = {'pk': id}
|
||||
try:
|
||||
user = User.objects.get(**query_kwargs)
|
||||
except ObjectDoesNotExist:
|
||||
msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!")
|
||||
sec_msg = _("目前只开放本站用户注册")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
'secondary_msg': sec_msg,
|
||||
}
|
||||
)
|
||||
if user == request.user:
|
||||
return redirect("common:home")
|
||||
else:
|
||||
# mastodon request
|
||||
relation = get_relationships([user.mastodon_id], request.session['oauth_token'])[0]
|
||||
if relation['blocked_by']:
|
||||
msg = _("你没有访问TA主页的权限😥")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
}
|
||||
)
|
||||
book_marks = BookMark.get_available_user_data(user, relation['following'])
|
||||
do_book_marks = book_marks.filter(status=MarkStatusEnum.DO)
|
||||
do_books_more = True if do_book_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
wish_book_marks = book_marks.filter(status=MarkStatusEnum.WISH)
|
||||
wish_books_more = True if wish_book_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
collect_book_marks = book_marks.filter(status=MarkStatusEnum.COLLECT)
|
||||
collect_books_more = True if collect_book_marks.count() > BOOKS_PER_SET else False
|
||||
return render(
|
||||
request,
|
||||
'common/home.html',
|
||||
{
|
||||
'user': user,
|
||||
'do_book_marks': do_book_marks[:BOOKS_PER_SET],
|
||||
'wish_book_marks': wish_book_marks[:BOOKS_PER_SET],
|
||||
'collect_book_marks': collect_book_marks[:BOOKS_PER_SET],
|
||||
'do_books_more': do_books_more,
|
||||
'wish_books_more': wish_books_more,
|
||||
'collect_books_more': collect_books_more,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def followers(request, id):
|
||||
if request.method == 'GET':
|
||||
try:
|
||||
user = User.objects.get(pk=id)
|
||||
except ObjectDoesNotExist:
|
||||
msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!")
|
||||
sec_msg = _("目前只开放本站用户注册")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
'secondary_msg': sec_msg,
|
||||
}
|
||||
)
|
||||
# mastodon request
|
||||
if not user == request.user:
|
||||
relation = get_relationships([user.mastodon_id], request.session['oauth_token'])[0]
|
||||
if relation['blocked_by']:
|
||||
msg = _("你没有访问TA主页的权限😥")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
}
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
'users/list.html',
|
||||
{
|
||||
'user': user,
|
||||
'is_followers_page': True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def following(request, id):
|
||||
if request.method == 'GET':
|
||||
try:
|
||||
user = User.objects.get(pk=id)
|
||||
except ObjectDoesNotExist:
|
||||
msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!")
|
||||
sec_msg = _("目前只开放本站用户注册")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
'secondary_msg': sec_msg,
|
||||
}
|
||||
)
|
||||
# mastodon request
|
||||
if not user == request.user:
|
||||
relation = get_relationships([user.mastodon_id], request.session['oauth_token'])[0]
|
||||
if relation['blocked_by']:
|
||||
msg = _("你没有访问TA主页的权限😥")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
}
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
'users/list.html',
|
||||
{
|
||||
'user': user,
|
||||
'page_type': 'followers',
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def book_list(request, id, status):
|
||||
if request.method == 'GET':
|
||||
if not status.upper() in MarkStatusEnum.names:
|
||||
return HttpResponseBadRequest()
|
||||
try:
|
||||
user = User.objects.get(pk=id)
|
||||
except ObjectDoesNotExist:
|
||||
msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!")
|
||||
sec_msg = _("目前只开放本站用户注册")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
'secondary_msg': sec_msg,
|
||||
}
|
||||
)
|
||||
# mastodon request
|
||||
if not user == request.user:
|
||||
relation = get_relationships([user.mastodon_id], request.session['oauth_token'])[0]
|
||||
if relation['blocked_by']:
|
||||
msg = _("你没有访问TA主页的权限😥")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
}
|
||||
)
|
||||
queryset = BookMark.get_available_user_data(user, relation['is_following']).filter(status=MarkStatusEnum[status.upper()])
|
||||
else:
|
||||
queryset = BookMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])
|
||||
paginator = Paginator(queryset, ITEMS_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
marks = paginator.get_page(page_number)
|
||||
return render(
|
||||
request,
|
||||
'users/list.html',
|
||||
{
|
||||
'marks': marks,
|
||||
'user': user,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def report(request):
|
||||
if request.method == 'GET':
|
||||
form = ReportForm()
|
||||
return render(
|
||||
request,
|
||||
'users/report.html',
|
||||
{
|
||||
'form': form,
|
||||
}
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
form = ReportForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.instance.is_read = False
|
||||
form.instance.submit_user = request.user
|
||||
form.save()
|
||||
return redirect(reverse("users:home", args=[form.instance.reported_user.id]))
|
||||
else:
|
||||
return render(
|
||||
request,
|
||||
'users/report.html',
|
||||
{
|
||||
'form': form,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_report(request):
|
||||
if request.method == 'GET':
|
||||
reports = Report.objects.all()
|
||||
for r in reports.filter(is_read=False):
|
||||
r.save()
|
||||
return render(
|
||||
request,
|
||||
'users/manage_report.html',
|
||||
{
|
||||
'reports': reports,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
# Utils
|
||||
########################################
|
||||
|
||||
def auth_login(request, user, token):
|
||||
""" Decorates django ``login()``. Attach token to session."""
|
||||
request.session['oauth_token'] = token
|
||||
|
|
Loading…
Add table
Reference in a new issue