second commit

This commit is contained in:
doubaniux 2020-05-05 23:50:48 +08:00
parent d219921bb0
commit 8ddbb9d257
52 changed files with 3889 additions and 306 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
$(document).ready( function() {
$("#submit").click(function(e) {
e.preventDefault();
$("#scrapeForm form").submit();
});
});

View file

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

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

View file

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

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

View file

@ -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 }}
@ -58,7 +63,7 @@
<div>{% if book.translator %}{% trans '译者:' %}
{% for translator in book.translator %}
<span>{{ translator }}</span>
{% endfor %}
{% endfor %}
{% endif %}</div>
<div>{% if book.orig_title %}{% trans '原作名:' %}{{ book.orig_title }}{% endif %}</div>
<div>{% if book.language %}{% trans '语言:' %}{{ book.language }}{% endif %}</div>
@ -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,16 +115,136 @@
{% 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) {

View file

View 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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<span class="page-index">
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
</span>
&nbsp;&nbsp;<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>
&nbsp;&nbsp;<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>

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

View 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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<span class="page-index">
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
</span>
&nbsp;&nbsp;<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>
&nbsp;&nbsp;<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>

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
from .decorators import *

View file

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

View 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

View file

@ -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
@ -73,4 +158,4 @@ class Review(UserOwnedEntity):
return self.title
class Meta:
abstract = True
abstract = True

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

View file

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

View file

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

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

View file

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

View 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,
});
});
});

View file

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

View file

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

View file

@ -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);
},
});
}
@ -73,9 +78,12 @@ function getFollowing(id, mastodonURI, token, callback) {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
},
data: {
'limit': NUMBER_PER_REQUEST
},
success: function(data){
callback(data);
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',

View file

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

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required
from books.models import Book
from common.models import MarkStatusEnum
from users.models import Report, User
from django.core.paginator import Paginator
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpResponseBadRequest
@ -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...

View file

@ -1,3 +1,6 @@
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(Report)
admin.site.register(User)

View file

@ -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
View 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': _("详情")}),
}

View file

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

View 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;
},
});
}
}
});
});

View 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;
},
});
}
}
});
});

View 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 %}
&nbsp;{% 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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<span class="page-index">
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
</span>
&nbsp;&nbsp;<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>
&nbsp;&nbsp;<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>

View file

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

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

View file

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

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

View file

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

View file

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