add douban movie scraper & movie app skeleton

This commit is contained in:
doubaniux 2020-09-29 21:46:21 +02:00
parent 3a83c954cd
commit ac9289c289
19 changed files with 2532 additions and 37 deletions

View file

@ -24,6 +24,7 @@ urlpatterns = [
path('markdownx/', include('markdownx.urls')),
path('users/', include('users.urls')),
path('books/', include('books.urls')),
path('movies/', include('movies.urls')),
path('', include('common.urls')),
]

View file

@ -3,10 +3,13 @@ import random
from lxml import html
import re
from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG
from movies.models import MovieGenreEnum
RE_NUMBERS = re.compile(r"\d+\d*")
RE_WHITESPACES = re.compile(r"\s+")
RE_DOUBAN_BOOK_URL = re.compile(r"https://book.douban.com/subject/\d+/")
# without slash at the end
RE_DOUBAN_BOOK_URL = re.compile(r"https://book.douban.com/subject/\d+")
RE_DOUBAN_MOVIE_URL = re.compile(r"https://movie.douban.com/subject/\d+")
DEFAULT_REQUEST_HEADERS = {
'Host': 'book.douban.com',
@ -18,7 +21,7 @@ DEFAULT_REQUEST_HEADERS = {
'Connection': 'keep-alive',
'DNT': '1',
'Upgrade-Insecure-Requests': '1',
'Cache-Control': 'max-age=0',
'Cache-Control': 'no-cache',
}
# in seconds
@ -28,24 +31,67 @@ TIMEOUT = 10
PORT = 22225
def scrape_douban_book(url):
if RE_DOUBAN_BOOK_URL.match(url) is None:
raise ValueError("not valid douban book url")
def download_page(url, regex, headers):
url = regex.findall(url)
if not url:
raise ValueError("not valid url")
else:
url = url[0] + '/'
session_id = random.random()
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
proxies = {
'http': proxy_url,
'https': proxy_url,
'https': proxy_url,
}
if DEBUG:
proxies = None
r = requests.get(url, proxies=proxies, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT)
# if DEBUG:
# proxies = None
r = requests.get(url, proxies=proxies, headers=headers, timeout=TIMEOUT)
# r = requests.get(url, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT)
content = html.fromstring(r.content.decode('utf-8'))
return html.fromstring(r.content.decode('utf-8'))
def download_image(url):
if url is None:
return
raw_img = None
session_id = random.random()
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
proxies = {
'http': proxy_url,
'https': proxy_url,
}
# if DEBUG:
# proxies = None
if url:
img_response = requests.get(
url,
headers={
'accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'accept-encoding': 'gzip, deflate',
'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,fr-FR;q=0.6,fr;q=0.5,zh-TW;q=0.4',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edg/81.0.416.72',
'cache-control': 'no-cache',
'dnt': '1',
},
proxies=proxies,
timeout=TIMEOUT,
)
if img_response.status_code == 200:
raw_img = img_response.content
return raw_img
def scrape_douban_book(url):
regex = RE_DOUBAN_BOOK_URL
headers = DEFAULT_REQUEST_HEADERS.copy()
headers['Host'] = 'book.douban.com'
content = download_page(url, regex, headers)
# parsing starts here
try:
title = content.xpath("/html/body/div[3]/h1/span/text()")[0].strip()
except IndexError:
@ -111,23 +157,7 @@ def scrape_douban_book(url):
img_url_elem = content.xpath("//*[@id='mainpic']/a/img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img = None
if img_url:
img_response = requests.get(
img_url,
headers={
'accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'accept-encoding': 'gzip, deflate',
'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,fr-FR;q=0.6,fr;q=0.5,zh-TW;q=0.4',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edg/81.0.416.72',
'cache-control': 'no-cache',
'dnt': '1' ,
},
proxies=proxies,
timeout=TIMEOUT,
)
if img_response.status_code == 200:
raw_img = img_response.content
raw_img = download_image(img_url)
# there are two html formats for authors and translators
authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
@ -182,3 +212,143 @@ def scrape_douban_book(url):
'other_info' : other
}
return data, raw_img
def scrape_douban_movie(url):
regex = RE_DOUBAN_MOVIE_URL
headers = DEFAULT_REQUEST_HEADERS.copy()
headers['Host'] = 'movie.douban.com'
content = download_page(url, regex, headers)
# parsing starts here
try:
raw_title = content.xpath(
"//span[@property='v:itemreviewed']/text()")[0].strip()
except IndexError:
raise ValueError("given url contains no movie info")
orig_title = content.xpath(
"//img[@rel='v:image']/@alt")[0].strip()
title = raw_title.split(orig_title)[0].strip()
# if has no chinese title
if title == '':
title = orig_title
# there are two html formats for authors and translators
other_title_elem = content.xpath(
"//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None
imbd_elem = content.xpath(
"//div[@id='info']//span[text()='IMDb链接:']/following-sibling::a[1]/text()")
imbd_code = imbd_elem[0].strip() if imbd_elem else None
director_elem = content.xpath("//div[@id='info']//span[text()='导演']/following-sibling::span[1]/a/text()")
director = director_elem if director_elem else None
playwright_elem = content.xpath("//div[@id='info']//span[text()='编剧']/following-sibling::span[1]/a/text()")
playwright = playwright_elem if playwright_elem else None
actor_elem = content.xpath("//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()")
actor = actor_elem if actor_elem else None
# construct genre translator
genre_translator = {}
attrs = [attr for attr in dir(MovieGenreEnum) if not '__' in attr]
for attr in attrs:
genre_translator[getattr(MovieGenreEnum, attr).label] = getattr(
MovieGenreEnum, attr).value
genre_elem = content.xpath("//span[@property='v:genre']/text()")
if genre_elem:
genre = []
for g in genre_elem:
genre.append(genre_translator[g])
else:
genre = None
showtime_elem = content.xpath("//span[@property='v:initialReleaseDate']/text()")
if showtime_elem:
showtime = []
for st in showtime_elem:
time = st.split('(')[0]
region = st.split('(')[1][0:-1]
showtime.append({time: region})
else:
showtime = None
site_elem = content.xpath("//div[@id='info']//span[text()='官方网站:']/following-sibling::a[1]/@href")
site = site_elem[0].strip() if site_elem else None
area_elem = content.xpath("//div[@id='info']//span[text()='制片国家/地区:']/following-sibling::text()[1]")
if area_elem:
area = [a.strip() for a in area_elem[0].split(' / ')]
else:
area = None
language_elem = content.xpath("//div[@id='info']//span[text()='语言:']/following-sibling::text()[1]")
if language_elem:
language = [a.strip() for a in language_elem[0].split(' / ')]
else:
language = None
year_elem = content.xpath("//span[@class='year']/text()")
year = int(year_elem[0][1:-1]) if year_elem else None
duration_elem = content.xpath("//span[@property='v:runtime']/text()")
other_duration_elem = content.xpath("//span[@property='v:runtime']/following-sibling::text()[1]")
if duration_elem:
duration = duration_elem[0].strip()
if other_duration_elem:
duration += other_duration_elem[0]
else:
duration = None
season_elem = content.xpath("//*[@id='season']/option[@selected='selected']/text()")
if not season_elem:
season_elem = content.xpath(
"//div[@id='info']//span[text()='季数:']/following-sibling::text()[1]")
season = int(season_elem[0].strip()) if season_elem else None
else:
season = int(season_elem[0].strip())
episodes_elem = content.xpath(
"//div[@id='info']//span[text()='集数:']/following-sibling::text()[1]")
episodes = int(episodes_elem[0].strip()) if episodes_elem else None
single_episode_length_elem = content.xpath(
"//div[@id='info']//span[text()='单集片长:']/following-sibling::text()[1]")
single_episode_length = int(single_episode_length_elem[0].strip()) if single_episode_length_elem else None
# if has field `episodes` not none then must be series
is_series = True if episodes else False
brief_elem = content.xpath("//span[@property='v:summary']/text()")
brief = brief[0].strip() if brief_elem else None
img_url_elem = content.xpath("//img[@rel='v:image']/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img = download_image(img_url)
data = {
'title': title,
'orig_title': orig_title,
'other_title': other_title,
'imbd_code': imbd_code,
'director': director,
'playwright': playwright,
'actor': actor,
'genre': genre,
'showtime': showtime,
'site': site,
'area': area,
'language': language,
'year': year,
'duration': duration,
'season': season,
'episodes': episodes,
'single_episode_length': single_episode_length,
'brief': brief,
'is_series': is_series,
}
return data, raw_img

0
movies/__init__.py Normal file
View file

5
movies/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MoviesConfig(AppConfig):
name = 'movies'

179
movies/forms.py Normal file
View file

@ -0,0 +1,179 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _
from .models import Movie, MovieMark, MovieReview
from common.models import MarkStatusEnum
from common.forms import RadioBooleanField, RatingValidator, TagField, TagInput
from common.forms import KeyValueInput
from common.forms import PreviewImageInput
def MovieMarkStatusTranslator(status):
trans_dict = {
MarkStatusEnum.DO.value: _("在看"),
MarkStatusEnum.WISH.value: _("想看"),
MarkStatusEnum.COLLECT.value: _("看过")
}
return trans_dict[status]
class MovieForm(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 = Movie
fields = [
'id',
'title',
'orig_title',
'other_title',
'imbd_code',
'director',
'playwright',
'actor',
'genre',
'showtime',
'site',
'area',
'language',
'year',
'duration',
'season',
'episodes',
'single_episode_length',
'cover',
'brief',
'is_series',
]
labels = {
'title': _("标题"),
'orig_title': _("原名"),
'other_title': _("又名"),
'imbd_code': _("IMBd编号"),
'director': _("导演"),
'playwright': _("编剧"),
'actor': _("主演"),
'genre': _("类型"),
'showtime': _("上映时间"),
'site': _("官方网站"),
'area': _("国家/地区"),
'language': _("语言"),
'year': _("年份"),
'duration': _("片长"),
'season': _("季数"),
'episodes': _("集数"),
'single_episode_length': _("单集片长"),
'cover': _("封面"),
'brief': _("简介"),
'is_series': _("是否是剧集"),
}
# widgets = {
# 'author': forms.TextInput(attrs={'placeholder': _("多个作者使用英文逗号分隔")}),
# 'translator': forms.TextInput(attrs={'placeholder': _("多个译者使用英文逗号分隔")}),
# 'other_info': KeyValueInput(),
# # 'cover': forms.FileInput(),
# 'cover': PreviewImageInput(),
# }
# def clean_isbn(self):
# isbn = self.cleaned_data.get('isbn')
# if isbn:
# isbn = isbn.strip()
# return isbn
class MovieMarkForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
STATUS_CHOICES = [(v, MovieMarkStatusTranslator(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
)
tags = TagField(
required=False,
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
label=_("标签")
)
text = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"placeholder": _("最多只能写360字哦~"),
"maxlength": 360
}
),
label=_("短评"),
)
class Meta:
model = MovieMark
fields = [
'id',
'movie',
'status',
'rating',
'text',
'is_private',
]
labels = {
'rating': _("评分"),
}
widgets = {
'movie': forms.TextInput(attrs={"hidden": ""}),
}
class MovieReviewForm(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 = MovieReview
fields = [
'id',
'movie',
'title',
'content',
'is_private'
]
labels = {
'book': "",
'title': _("标题"),
'content': _("正文"),
'share_to_mastodon': _("分享到长毛象")
}
widgets = {
'movie': forms.TextInput(attrs={"hidden": ""}),
}

View file

@ -4,15 +4,22 @@ 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, Tag
from boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE
from boofilsic.settings import MOVIE_MEDIA_PATH_ROOT, DEFAULT_MOVIE_IMAGE
from django.utils import timezone
def movie_cover_path():
pass
def movie_cover_path(instance, filename):
ext = filename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
root = ''
if MOVIE_MEDIA_PATH_ROOT.endswith('/'):
root = MOVIE_MEDIA_PATH_ROOT
else:
root = MOVIE_MEDIA_PATH_ROOT + '/'
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
class GenreEnum(models.TextChoices):
class MovieGenreEnum(models.TextChoices):
DRAMA = 'Drama', _('剧情')
KIDS = 'Kids', _('儿童')
COMEDY = 'Comedy', _('喜剧')
@ -90,7 +97,8 @@ class Movie(Resource):
_("genre"),
blank=True,
default='',
choices=GenreEnum.choices
choices=MovieGenreEnum.choices,
max_length=50
)
showtime = postgres.ArrayField(
# HStoreField stores showtime-region pair
@ -101,9 +109,10 @@ class Movie(Resource):
)
site = models.URLField(_('site url'), max_length=200)
# country or area
# country or region
area = postgres.ArrayField(
models.CharField(
_("country or region"),
blank=True,
default='',
max_length=100,
@ -127,13 +136,17 @@ class Movie(Resource):
year = models.PositiveIntegerField(null=True, blank=True)
duration = models.CharField(blank=True, default='', max_length=100)
cover = models.ImageField(_("poster"), upload_to=movie_cover_path, default=DEFAULT_MOVIE_IMAGE, blank=True)
############################################
# exclusive fields to series
############################################
season = models.PositiveSmallIntegerField(null=True, blank=True)
# how many episodes in the season
episodes = models.PositiveIntegerField(null=True, blank=True)
tv_station = models.CharField(blank=True, default='', max_length=200)
# deprecated
# tv_station = models.CharField(blank=True, default='', max_length=200)
single_episode_length = models.CharField(blank=True, default='', max_length=100)
############################################
# category identifier
@ -146,7 +159,7 @@ class Movie(Resource):
def get_tags_manager(self):
raise NotImplementedError
return self.movie_tags
class MovieMark(Mark):

View file

@ -0,0 +1,58 @@
{% 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 'Nicedb - ' %}{{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
<!-- <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.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
<div class="single-section-wrapper" id="main">
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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>
// mark required
$("#content input[required]").each(function () {
$(this).prev().prepend("*");
})
</script>
</body>
</html>

View file

@ -0,0 +1,112 @@
{% 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 'Nicedb - ' %}{{ 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 'css/boofilsic.css' %}">
<!-- <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}"> -->
<!-- <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}"> -->
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper">
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'movies:retrieve' movie.id %}">
<img src="{{ movie.cover.url }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<h5 class="entity-card__title"><a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a></h5>
<div>{% if movie.isbn %}{% trans 'ISBN' %}{{ movie.isbn }}{% endif %}</div>
<div>{% if movie.author %}{% trans '作者:' %}
{% for author in movie.author %}
<span>{{ author }}</span>
{% endfor %}
{% endif %}</div>
<div>{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}</div>
<div>{%if movie.pub_year %}{% trans '出版时间:' %}{{ movie.pub_year }}{% trans '年' %}{% if movie.pub_month %}{{ movie.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
{% if movie.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ movie.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.movie }}
<div>
{{ form.title.label }}
</div>
{{ form.title }}
<div class="clearfix">
<span class="float-left">
{{ form.content.label }}
</span>
<span class="float-right">
<span class="review-form__preview-button">{% trans '预览' %}</span>
</span>
</div>
<div id="rawContent">
{{ form.content }}
</div>
<div class="review-form__fyi">{% trans '不知道什么是Markdown可以参考' %}<a target="_blank" href="https://www.markdownguide.org/">{% trans '这里' %}</a></div>
<div class="review-form__option">
<div class="review-form__visibility-radio">
{{ form.is_private.label }}{{ form.is_private }}
</div>
<div class="review-form__share-checkbox">
{{ 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>
{% include "partial/_footer.html" %}
</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>
</script>
</body>
</html>

View file

@ -0,0 +1,92 @@
{% 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 'Nicedb - 删除电影/剧集' %}</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' %}"> -->
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这部电影/剧集吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'movies:retrieve' movie.id %}">
<img src="{{ movie.cover.url }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'movies:retrieve' movie.id %}">
<h5 class="entity-card__title">
{{ movie.title }}
</h5>
</a>
{% if movie.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ movie.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if movie.last_editor %}
<a href="{% url 'users:home' movie.last_editor.id %}">
<div>{% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}</div>
</a>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ movie.edited_time }}</div>
</div>
</div>
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'movies:delete' movie.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>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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>
</script>
</body>
</html>

View file

@ -0,0 +1,110 @@
{% 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 'Nicedb - 删除评论' %}</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 'css/boofilsic.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">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这篇评论吗?' %}</h5>
<div class="dividing-line"></div>
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% 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 %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.id %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'movies: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>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -0,0 +1,385 @@
{% 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">
<meta property="og:title" content="NiceDB书 - {{ movie.title }}">
<meta property="og:type" content="movie">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ movie.cover.url }}">
{% if movie.author %}
<meta property="og:movie:author" content="{% for author in movie.author %}{{ author }}{% if not forloop.last %},{% endif %}{% endfor %}">
{% endif %}
{% if movie.isbn %}
<meta property="og:movie:isbn" content="{{ movie.isbn }}">
{% endif %}
<title>{% trans 'Nicedb - 电影/剧集详情' %} | {{ movie.title }}</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/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/milligram.css' %}"> -->
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-detail">
<img src="{{ movie.cover.url }}" class="entity-detail__img" alt="{{ movie.title }}">
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ movie.title }}
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if movie.rating %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ movie.rating }} </span>
{% else %}
<span> {% trans '评分:暂无评分' %}</span>
{% endif %}
</div>
<div>{% if movie.isbn %}{% trans 'ISBN' %}{{ movie.isbn }}{% endif %}</div>
<div>{% if movie.author %}{% trans '作者:' %}
{% for author in movie.author %}
<span>{{ author }}</span>
{% endfor %}
{% endif %}</div>
<div>{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}</div>
<div>{% if movie.subtitle %}{% trans '副标题:' %}{{ movie.subtitle }}{% endif %}</div>
<div>{% if movie.translator %}{% trans '译者:' %}
{% for translator in movie.translator %}
<span>{{ translator }}</span>
{% endfor %}
{% endif %}</div>
<div>{% if movie.orig_title %}{% trans '原作名:' %}{{ movie.orig_title }}{% endif %}</div>
<div>{% if movie.language %}{% trans '语言:' %}{{ movie.language }}{% endif %}</div>
<div>{%if movie.pub_year %}{% trans '出版时间:' %}{{ movie.pub_year }}{% trans '年' %}{% if movie.pub_month %}{{ movie.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
</div>
<div class="entity-detail__fields">
<div>{% if movie.binding %}{% trans '装帧:' %}{{ movie.binding }}{% endif %}</div>
<div>{% if movie.price %}{% trans '定价:' %}{{ movie.price }}{% endif %}</div>
<div>{% if movie.pages %}{% trans '页数:' %}{{ movie.pages }}{% endif %}</div>
{% if movie.other_info %}
{% for k, v in movie.other_info.items %}
<div>
{{k}}{{v}}
</div>
{% endfor %}
{% endif %}
{% if movie.last_editor %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.id %}">{{ movie.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'movies:update' movie.id %}">{% trans '编辑这部电影/剧集' %}</a>
{% if user.is_staff %}
<a href="{% url 'movies:delete' movie.id %}"> / {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in movie_tag_list %}
{% for k, v in tag_dict.items %}
{% if k == 'content' %}
<span class="tag-collection__tag">
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
</span>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</div>
</div>
<div class="dividing-line"></div>
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
{% if movie.brief %}
<p class="entity-desc__content">{{ movie.brief | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
{% else %}
<div>{% trans '暂无简介' %}</div>
{% endif %}
</div>
{% if movie.contents %}
<div class="entity-desc" id="contents">
<h5 class="entity-desc__title">{% trans '目录' %}</h5>
<p class="entity-desc__content">{{ movie.contents | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
<div class="entity-marks">
<h5 class="entity-marks__title">{% trans '这部电影/剧集的标记' %}</h5>
{% if mark_list_more %}
<a href="{% url 'movies:retrieve_mark_list' movie.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
{% endif %}
{% if mark_list %}
<ul class="entity-marks__mark-list">
{% for others_mark in mark_list %}
<li class="entity-marks__mark">
<a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
<span>{{ others_mark.get_status_display }}</span>
{% if others_mark.rating %}
<span class="entity-marks__rating-star 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="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
{% if others_mark.text %}
<p class="entity-marks__mark-content">{{ others_mark.text }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div>{% trans '暂无标记' %}</div>
{% endif %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这部电影/剧集的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'movies:retrieve_review_list' movie.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
{% endif %}
{% if review_list %}
<ul class="entity-reviews__review-list">
{% for others_review in review_list %}
<li class="entity-reviews__review">
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ 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="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
</li>
{% endfor %}
</ul>
{% else %}
<div>{% trans '暂无评论' %}</div>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
{% if mark %}
<div class="mark-panel">
<span class="mark-panel__status">{% 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="mark-panel__rating-star 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="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'movies:delete_mark' mark.id %}" method="post">
{% csrf_token %}
<a href="" class="delete">{% trans '删除' %}</a>
</form>
</span>
<div class="mark-panel__clear"></div>
<div class="mark-panel__time">{{ mark.edited_time }}</div>
{% if mark.text %}
<p class="mark-panel__text">{{ mark.text }}</p>
{% endif %}
<div class="tag-collection">
{% for tag in mark_tags %}
<span class="tag-collection__tag">{{ tag }}</span>
{% endfor %}
</div>
</div>
{% else %}
<div class="action-panel" id="addMarkPanel">
<div class="action-panel__label">{% trans '标记这部电影/剧集' %}</div>
<div class="action-panel__button-group">
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想读' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在读' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '读过' %}</button>
</div>
</div>
{% endif %}
</div>
<div class="aside-section-wrapper">
{% if review %}
<div class="review-panel">
<span class="review-panel__label">{% 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="review-panel__actions">
<a href="{% url 'movies:update_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'movies:delete_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'movies:retrieve_review' review.id %}" class="review-panel__review-title">
{{ review.title }}
</a>
</div>
{% else %}
<div class="action-panel">
<div class="action-panel__label">{% trans '我的评论' %}</div>
<div class="action-panel__button-group action-panel__button-group--center">
<a href="{% url 'movies:create_review' movie.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<div id="modals">
<div class="mark-modal modal">
<div class="mark-modal__head">
{% if not mark %}
<style>
.mark-modal__title::after {
content: "{% trans '这部电影/剧集' %}";
}
</style>
<span class="mark-modal__title"></span>
{% else %}
<span class="mark-modal__title">{% trans '我的标记' %}</span>
{% endif %}
<span class="mark-modal__close-button modal-close">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="mark-modal__body">
<form action="{% url 'movies:create_update_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.movie }}
{% if mark.rating %}
{% endif %}
<div class="mark-modal__rating-star rating-star-edit"></div>
{{ mark_form.rating }}
<div id="statusSelection" class="mark-modal__status-radio" {% if not mark %}hidden{% endif %}>
{{ mark_form.status }}
</div>
<div class="mark-modal__clear"></div>
{{ mark_form.text }}
<div class="mark-modal__tag">
<label>{{ mark_form.tags.label }}</label>
{{ mark_form.tags }}
</div>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>{{ mark_form.is_private.label }}:</span>
{{ mark_form.is_private }}
</div>
<div class="mark-modal__share-checkbox">
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
</div>
</div>
<div class="mark-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '提交' %}">
</div>
</form>
</div>
</div>
<div class="confirm-modal modal">
<div class="confirm-modal__head">
<span class="confirm-modal__title">{% trans '确定要删除你的标记吗?' %}</span>
<span class="confirm-modal__close-button modal-close">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="confirm-modal__body">
<div class="confirm-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '确认' %}">
</div>
</div>
</div>
</div>
<div class="bg-mask"></div>
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,277 @@
{% 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 'Nicedb - ' %}{{ user.username }}{{ list_title }}</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 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
<!-- <link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}"> -->
<!-- <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}"> -->
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid grid--reverse-order">
<div class="grid__main grid__main--reverse-order">
<div class="main-section-wrapper">
<div class="entity-list">
<div class="set">
<h5 class="entity-list__title">
{{ user.username }}{{ list_title }}
</h5>
</div>
<ul class="entity-list__entities">
{% for mark in marks %}
<li class="entity-list__entity">
<div class="entity-list__entity-img-wrapper">
<a href="{% url 'movies:retrieve' mark.movie.id %}">
<img src="{{ mark.movie.cover.url }}" alt="" class="entity-list__entity-img">
</a>
</div>
<div class="entity-list__entity-text">
<div class="entity-list__entity-title">
<a href="{% url 'movies:retrieve' mark.movie.id %}" class="entity-list__entity-link">
{{ mark.movie.title }}
</a>
</div>
{% comment %}
<!-- {% if mark.movie.rating %}
<div class="rating-star entity-list__rating-star" data-rating-score="{{ mark.movie.rating | floatformat:"0" }}"></div>
<span class="entity-list__rating-score rating-score">
{{ mark.movie.rating }}
</span>
{% else %}
<div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
{% endif %} -->
{% endcomment %}
<span class="entity-list__entity-info entity-list__entity-info--full-length">
{% if mark.movie.pub_year %}
{{ mark.movie.pub_year }}{% trans '年' %} /
{% if mark.movie.pub_month %}
{{ mark.movie.pub_month }}{% trans '月' %} /
{% endif %}
{% endif %}
{% if mark.movie.author %}
{% trans '作者' %}
{% for author in mark.movie.author %}
{{ author }}{% if not forloop.last %},{% endif %}
{% endfor %}/
{% endif %}
{% if mark.movie.translator %}
{% trans '译者' %}
{% for translator in mark.movie.translator %}
{{ translator }}{% if not forloop.last %},{% endif %}
{% endfor %}/
{% endif %}
{% if mark.movie.orig_title %}
&nbsp;{% trans '原名' %}
{{ mark.movie.orig_title }}
{% endif %}
</span>
<p class="entity-list__entity-brief">
{{ mark.movie.brief | truncate:170 }}
</p>
<div class="tag-collection">
{% for tag_dict in mark.movie.tag_list %}
{% for k, v in tag_dict.items %}
{% if k == 'content' %}
<span class="tag-collection__tag">
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
</span>
{% endif %}
{% endfor %}
{% endfor %}
</div>
<div class="clearfix"></div>
<div class="dividing-line dividing-line--dashed"></div>
<div class="entity-marks" style="margin-bottom: 0;">
<ul class="entity-marks__mark-list">
<li class="entity-marks__mark">
{% if mark.rating %}
<span class="entity-marks__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
{% endif %}
{% if mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
{% if mark.text %}
<p class="entity-marks__mark-content">{{ mark.text }}</p>
{% endif %}
</li>
</ul>
</div>
</div>
</li>
{% empty %}
<div>{% trans '无结果' %}</div>
{% endfor %}
<!-- user mark -->
</ul>
</div>
<div class="pagination">
{% if marks.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ marks.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in marks.pagination.page_range %}
{% if page == marks.pagination.current_page %}
<a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
{% else %}
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
{% endif %}
{% endfor %}
{% if marks.pagination.has_next %}
<a href="?page={{ marks.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
<div class="aside-section-wrapper aside-section-wrapper--no-margin">
<div class="user-profile" id="userInfoCard">
<div class="user-profile__header">
<!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
<img src="" class="user-profile__avatar mast-avatar">
<a href="{% url 'users:home' user.id %}">
<h5 class="user-profile__username mast-displayname"></h5>
</a>
</div>
<p class="user-profile__bio mast-brief"></p>
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
{% if request.user != user %}
<a href="{% url 'users:report' %}?user_id={{ user.id }}"
class="user-profile__report-link">{% trans '举报用户' %}</a>
{% endif %}
</div>
</div>
<div class="relation-dropdown">
<div class="relation-dropdown__button">
<span class="icon-arrow">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
</svg>
</span>
</div>
<div class="relation-dropdown__body">
<div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
<div class="user-relation" id="followings">
<h5 class="user-relation__label">
{% trans '关注的人' %}
</h5>
<a href="{% url 'users:following' user.id %}"
class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
<ul class="user-relation__related-user-list mast-following">
<li class="user-relation__related-user">
<a>
<img src="" alt="" class="user-relation__related-user-avatar">
<div class="user-relation__related-user-name mast-displayname">
</div>
</a>
</li>
</ul>
</div>
<div class="user-relation" id="followers">
<h5 class="user-relation__label">
{% trans '被他们关注' %}
</h5>
<a href="{% url 'users:followers' user.id %}"
class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
<ul class="user-relation__related-user-list mast-followers">
<li class="user-relation__related-user">
<a>
<img src="" alt="" class="user-relation__related-user-avatar">
<div class="user-relation__related-user-name mast-displayname">
</div>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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 %}?is_mastodon_id=true</div>
<div id="spinner" hidden>
<div class="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>
</script>
</body>
</html>

View file

@ -0,0 +1,145 @@
{% 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 'Nicedb - ' %}{{ movie.title }}{% trans '的标记' %}</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.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<!-- <link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}"> -->
<!-- <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}"> -->
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a>{% trans ' 的标记' %}
</h5>
<ul class="entity-marks__mark-list">
{% for mark in marks %}
<li class="entity-marks__mark entity-marks__mark--wider">
<a href="{% url 'users:home' mark.owner.id %}"
class="entity-marks__owner-link">{{ mark.owner.username }}</a>
<span>{{ mark.get_status_display }}</span>
{% if mark.rating %}
<span class="entity-marks__rating-star 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="entity-marks__mark-time">{{ mark.edited_time }}</span>
{% if mark.text %}
<p class="entity-marks__mark-content">{{ mark.text }}</p>
{% endif %}
</li>
{% empty %}
<div>
{% trans '无结果' %}
</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if marks.pagination.has_prev %}
<a href="?page=1"
class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ marks.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in marks.pagination.page_range %}
{% if page == marks.pagination.current_page %}
<a href="?page={{ page }}"
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
{% else %}
<a href="?page={{ page }}"
class="pagination__page-link">{{ page }}</a>
{% endif %}
{% endfor %}
{% if marks.pagination.has_next %}
<a href="?page={{ marks.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ marks.pagination.last_page }}"
class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'movies:retrieve' movie.id %}"><img src="{{ movie.cover.url }}" alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a></h5>
{% if movie.isbn %}
<div>ISBN: {{ movie.isbn }}</div>
{% endif %}
<div>{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}</div>
{% if movie.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ movie.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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>
</script>
</body>
</html>

View file

@ -0,0 +1,127 @@
{% 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">
<meta property="og:title" content="NiceDB影评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
<title>{% trans 'Nicedb - 评论详情' %}</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_browse.css' %}"> -->
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.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 %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link" href="{% url 'movies:update_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link" href="{% url 'movies:delete_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'movies:retrieve' movie.id %}"><img src="{{ movie.cover.url }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a></h5>
{% if movie.isbn %}
<div>ISBN: {{ movie.isbn }}</div>
{% endif %}
<div>{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}</div>
{% if movie.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ movie.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ movie.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -0,0 +1,132 @@
{% 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 'Nicedb - ' %}{{ movie.title }}{% trans '的评论' %}</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 'css/boofilsic.css' %}">
<!-- <link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}"> -->
<!-- <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}"> -->
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a>{% trans ' 的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ 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="entity-reviews__review-time">{{ review.edited_time }}</span>
<span href="{% url 'movies:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'movies:retrieve_review' review.id %}">{{ review.title }}</a></span>
</li>
{% empty %}
<div>{% trans '无结果' %}</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if reviews.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ reviews.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in reviews.pagination.page_range %}
{% if page == reviews.pagination.current_page %}
<a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
{% else %}
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
{% endif %}
{% endfor %}
{% if reviews.pagination.has_next %}
<a href="?page={{ reviews.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'movies:retrieve' movie.id %}"><img src="{{ movie.cover.url }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a></h5>
{% if movie.isbn %}
<div>ISBN: {{ movie.isbn }}</div>
{% endif %}
<div>{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}</div>
{% if movie.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ movie.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ movie.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</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>
</script>
</body>
</html>

View file

@ -0,0 +1,88 @@
{% 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 'Nicedb - 从豆瓣获取数据' %}</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.css' %}">
<!-- <link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}"> -->
<!-- <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}"> -->
</head>
<body>
<style>
#scrape {
overflow: auto;
}
#scrape iframe {
width: 100%;
}
#scrape textarea {
height: 200px;
resize: vertical;
}
#scrape iframe {
height: 500px;
}
</style>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid grid--reverse-order">
<div class="grid__main grid__main--reverse-order" id="main">
<div class="main-section-wrapper">
<div id="scrape">
<h5>
{% trans '根据豆瓣内容填写下方表单' %}
</h5>
<iframe id='test' sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="https://search.douban.com/movie/subject_search{% if q %}?search_text={{ q }}{% endif %}" frameborder="0"></iframe>
<div class="dividing-line"></div>
<div id="scrapeForm">
<form action="{% url 'movies:create' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
</form>
<a href="{% url 'movies:scrape' %}" class="button add-button submit">{% trans '剽取!' %}</a>
</div>
</div>
</div>
</div>
<div class="grid__aside grid__aside--reverse-order" id="aside">
<div class="aside-section-wrapper aside-section-wrapper--singular">
<h5>
{% trans '复制详情页链接' %}
</h5>
<form action="{% url 'movies:click_to_scrape' %}" method="post">
{% csrf_token %}
<input type="text" name="url" required placeholder="https://movie.douban.com/subject/1000000/">
<input type="submit" class="button add-button" value="{% trans '一键剽取!' %}">
</form>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

22
movies/urls.py Normal file
View file

@ -0,0 +1,22 @@
from django.urls import path
from .views import *
app_name = 'movies'
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:movie_id>/mark/list/', retrieve_mark_list, name='retrieve_mark_list'),
path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
path('<int:movie_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:movie_id>/review/list/',
retrieve_review_list, name='retrieve_review_list'),
path('scrape/', scrape, name='scrape'),
path('click_to_scrape/', click_to_scrape, name='click_to_scrape'),
]

579
movies/views.py Normal file
View file

@ -0,0 +1,579 @@
import logging
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, HttpResponseServerError
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import IntegrityError, transaction
from django.db.models import Count
from django.utils import timezone
from django.core.paginator import Paginator
from django.core.files.uploadedfile import SimpleUploadedFile
from common.mastodon import mastodon_request_included
from common.mastodon.api import check_visibility, post_toot, TootVisibilityEnum
from common.mastodon.utils import rating_to_emoji
from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER
from .models import *
from .forms import *
from .forms import MovieMarkStatusTranslator
from boofilsic.settings import MASTODON_TAGS
logger = logging.getLogger(__name__)
mastodon_logger = logging.getLogger("django.mastodon")
# 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
# max tags on detail page
TAG_NUMBER = 10
# public data
###########################
@login_required
def create(request):
if request.method == 'GET':
form = MovieForm()
return render(
request,
'movies/create_update.html',
{
'form': form,
'title': _('添加书籍'),
'submit_url': reverse("movies:create")
}
)
elif request.method == 'POST':
if request.user.is_authenticated:
# only local user can alter public data
form = MovieForm(request.POST, request.FILES)
if form.is_valid():
form.instance.last_editor = request.user
form.save()
return redirect(reverse("movies:retrieve", args=[form.instance.id]))
else:
return render(
request,
'movies/create_update.html',
{
'form': form,
'title': _('添加书籍'),
'submit_url': reverse("movies:create")
}
)
else:
return redirect(reverse("users:login"))
else:
return HttpResponseBadRequest()
@login_required
def update(request, id):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=id)
form = MovieForm(instance=movie)
return render(
request,
'movies/create_update.html',
{
'form': form,
'title': _('修改书籍'),
'submit_url': reverse("movies:update", args=[movie.id])
}
)
elif request.method == 'POST':
movie = get_object_or_404(Movie, pk=id)
form = MovieForm(request.POST, request.FILES, instance=movie)
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
form.save()
else:
return render(
request,
'movies/create_update.html',
{
'form': form,
'title': _('修改书籍'),
'submit_url': reverse("movies:update", args=[movie.id])
}
)
return redirect(reverse("movies:retrieve", args=[form.instance.id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
# @login_required
def retrieve(request, id):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=id)
mark = None
mark_tags = None
review = None
# retreive tags
movie_tag_list = movie.movie_tags.values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER]
# retrieve user mark and initialize mark form
try:
if request.user.is_authenticated:
mark = MovieMark.objects.get(owner=request.user, movie=movie)
except ObjectDoesNotExist:
mark = None
if mark:
mark_tags = mark.mark_tags.all()
mark.get_status_display = MovieMarkStatusTranslator(mark.status)
mark_form = MovieMarkForm(instance=mark, initial={
'tags': mark_tags
})
else:
mark_form = MovieMarkForm(initial={
'movie': movie,
'tags': mark_tags
})
# retrieve user review
try:
if request.user.is_authenticated:
review = MovieReview.objects.get(owner=request.user, movie=movie)
except ObjectDoesNotExist:
review = None
# retrieve other related reviews and marks
if request.user.is_anonymous:
# hide all marks and reviews for anonymous user
mark_list = None
review_list = None
mark_list_more = None
review_list_more = None
else:
mark_list = MovieMark.get_available(
movie, request.user, request.session['oauth_token'])
review_list = MovieReview.get_available(
movie, 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 = MovieMarkStatusTranslator(m.status)
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,
'movies/detail.html',
{
'movie': movie,
'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,
'movie_tag_list': movie_tag_list,
'mark_tags': mark_tags,
}
)
else:
return HttpResponseBadRequest()
logger.warning('non-GET method at /movie/<id>')
@login_required
def delete(request, id):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=id)
return render(
request,
'movies/delete.html',
{
'movie': movie,
}
)
elif request.method == 'POST':
if request.user.is_staff:
# only staff has right to delete
movie = get_object_or_404(Movie, pk=id)
movie.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 movie rating
# owner check(guarantee)
if request.method == 'POST':
pk = request.POST.get('id')
old_rating = None
old_tags = None
if pk:
mark = get_object_or_404(MovieMark, pk=pk)
old_rating = mark.rating
old_tags = mark.mark_tags.all()
# update
form = MovieMarkForm(request.POST, instance=mark)
else:
# create
form = MovieMarkForm(request.POST)
if form.is_valid():
if form.instance.status == MarkStatusEnum.WISH.value:
form.instance.rating = None
form.cleaned_data['rating'] = None
form.instance.owner = request.user
form.instance.edited_time = timezone.now()
movie = form.instance.movie
try:
with transaction.atomic():
# update movie rating
movie.update_rating(old_rating, form.instance.rating)
form.save()
# update tags
if old_tags:
for tag in old_tags:
tag.delete()
if form.cleaned_data['tags']:
for tag in form.cleaned_data['tags']:
MovieTag.objects.create(
content=tag,
movie=movie,
mark=form.instance
)
except IntegrityError as e:
return HttpResponseServerError("integrity error")
logger.error(e.__str__())
if form.cleaned_data['share_to_mastodon']:
if form.cleaned_data['is_private']:
visibility = TootVisibilityEnum.PRIVATE
else:
visibility = TootVisibilityEnum.UNLISTED
url = "https://" + request.get_host() + reverse("movies:retrieve",
args=[movie.id])
words = MovieMarkStatusTranslator(int(form.cleaned_data['status'])) +\
f"{movie.title}" + \
rating_to_emoji(form.cleaned_data['rating'])
# tags = MASTODON_TAGS % {'category': '书', 'type': '标记'}
tags = ''
content = words + '\n' + url + '\n' + \
form.cleaned_data['text'] + '\n' + tags
response = post_toot(content, visibility,
request.session['oauth_token'])
if response.status_code != 200:
mastodon_logger.error(
f"CODE:{response.status_code} {response.text}")
return HttpResponseServerError("publishing mastodon status failed")
else:
return HttpResponseBadRequest("invalid form data")
return redirect(reverse("movies:retrieve", args=[form.instance.movie.id]))
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def retrieve_mark_list(request, movie_id):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=movie_id)
queryset = MovieMark.get_available(
movie, request.user, request.session['oauth_token'])
paginator = Paginator(queryset, MARK_PER_PAGE)
page_number = request.GET.get('page', default=1)
marks = paginator.get_page(page_number)
marks.pagination = PageLinksGenerator(
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
for m in marks:
m.get_status_display = MovieMarkStatusTranslator(m.status)
return render(
request,
'movies/mark_list.html',
{
'marks': marks,
'movie': movie,
}
)
else:
return HttpResponseBadRequest()
@login_required
def delete_mark(request, id):
if request.method == 'POST':
mark = get_object_or_404(MovieMark, pk=id)
movie_id = mark.movie.id
try:
with transaction.atomic():
# update movie rating
mark.movie.update_rating(mark.rating, None)
mark.delete()
except IntegrityError as e:
return HttpResponseServerError()
return redirect(reverse("movies:retrieve", args=[movie_id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def create_review(request, movie_id):
if request.method == 'GET':
form = MovieReviewForm(initial={'movie': movie_id})
movie = get_object_or_404(Movie, pk=movie_id)
return render(
request,
'movies/create_update_review.html',
{
'form': form,
'title': _("添加评论"),
'movie': movie,
'submit_url': reverse("movies:create_review", args=[movie_id]),
}
)
elif request.method == 'POST':
form = MovieReviewForm(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.UNLISTED
url = "https://" + request.get_host() + reverse("movies:retrieve_review",
args=[form.instance.id])
words = "发布了关于" + f"{form.instance.movie.title}" + "的评论"
# tags = MASTODON_TAGS % {'category': '书', 'type': '评论'}
tags = ''
content = words + '\n' + url + \
'\n' + form.cleaned_data['title'] + '\n' + tags
response = post_toot(content, visibility,
request.session['oauth_token'])
if response.status_code != 200:
mastodon_logger.error(
f"CODE:{response.status_code} {response.text}")
return HttpResponseServerError("publishing mastodon status failed")
return redirect(reverse("movies: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(MovieReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
form = MovieReviewForm(instance=review)
movie = review.movie
return render(
request,
'movies/create_update_review.html',
{
'form': form,
'title': _("编辑评论"),
'movie': movie,
'submit_url': reverse("movies:update_review", args=[review.id]),
}
)
elif request.method == 'POST':
review = get_object_or_404(MovieReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
form = MovieReviewForm(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.UNLISTED
url = "https://" + request.get_host() + reverse("movies:retrieve_review",
args=[form.instance.id])
words = "发布了关于" + f"{form.instance.movie.title}" + "的评论"
# tags = MASTODON_TAGS % {'category': '书', 'type': '评论'}
tags = ''
content = words + '\n' + url + \
'\n' + form.cleaned_data['title'] + '\n' + tags
response = post_toot(content, visibility,
request.session['oauth_token'])
if response.status_code != 200:
mastodon_logger.error(
f"CODE:{response.status_code} {response.text}")
return HttpResponseServerError("publishing mastodon status failed")
return redirect(reverse("movies: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(MovieReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
review_form = MovieReviewForm(instance=review)
return render(
request,
'movies/delete_review.html',
{
'form': review_form,
'review': review,
}
)
elif request.method == 'POST':
review = get_object_or_404(MovieReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
movie_id = review.movie.id
review.delete()
return redirect(reverse("movies:retrieve", args=[movie_id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def retrieve_review(request, id):
if request.method == 'GET':
review = get_object_or_404(MovieReview, pk=id)
if not check_visibility(review, request.session['oauth_token'], request.user):
msg = _("你没有访问这个页面的权限😥")
return render(
request,
'common/error.html',
{
'msg': msg,
}
)
review_form = MovieReviewForm(instance=review)
movie = review.movie
try:
mark = MovieMark.objects.get(owner=review.owner, movie=movie)
mark.get_status_display = MovieMarkStatusTranslator(mark.status)
except ObjectDoesNotExist:
mark = None
return render(
request,
'movies/review_detail.html',
{
'form': review_form,
'review': review,
'movie': movie,
'mark': mark,
}
)
else:
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def retrieve_review_list(request, movie_id):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=movie_id)
queryset = MovieReview.get_available(
movie, request.user, request.session['oauth_token'])
paginator = Paginator(queryset, REVIEW_PER_PAGE)
page_number = request.GET.get('page', default=1)
reviews = paginator.get_page(page_number)
reviews.pagination = PageLinksGenerator(
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
return render(
request,
'movies/review_list.html',
{
'reviews': reviews,
'movie': movie,
}
)
else:
return HttpResponseBadRequest()
@login_required
def scrape(request):
if request.method == 'GET':
keywords = request.GET.get('q')
form = MovieForm()
return render(
request,
'movies/scrape.html',
{
'q': keywords,
'form': form,
}
)
else:
return HttpResponseBadRequest()
@login_required
def click_to_scrape(request):
if request.method == "POST":
url = request.POST.get("url")
if url:
from common.scraper import scrape_douban_movie
try:
scraped_movie, raw_cover = scrape_douban_movie(url)
except TimeoutError:
return render(request, 'common/error.html', {'msg': _("爬取数据失败😫,请重试")})
except ValueError:
return render(request, 'common/error.html', {'msg': _("链接非法,爬取失败")})
scraped_cover = {
'cover': SimpleUploadedFile('temp.jpg', raw_cover)}
form = MovieForm(scraped_movie, scraped_cover)
if form.is_valid():
form.instance.last_editor = request.user
form.save()
return redirect(reverse('movies:retrieve', args=[form.instance.id]))
else:
if 'isbn' in form.errors:
msg = _("ISBN与现有图书重复")
else:
msg = _("爬取数据失败😫")
return render(request, 'common/error.html', {'msg': msg})
else:
return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()