remove pre-0.5 apps

This commit is contained in:
Your Name 2023-01-11 09:41:55 -05:00
parent b5632f4844
commit 3439fb93e8
98 changed files with 2 additions and 13437 deletions

View file

@ -23,7 +23,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: ['3.10']
python-version: ['3.10', '3.11']
steps:
- uses: actions/checkout@v3
@ -35,11 +35,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends postgresql-client
PGPASSWORD=admin123 psql template1 -U postgres -h localhost -c 'create extension hstore;'
- name: Run Tests
run: |
python manage.py makemigrations contenttypes auth mastodon users common management catalog journal social legacy books movies games music collection
python manage.py makemigrations contenttypes auth mastodon users common management catalog journal social legacy
python manage.py test

View file

@ -69,15 +69,6 @@ INSTALLED_APPS += [
"legacy.apps.LegacyConfig",
]
INSTALLED_APPS += [
"books.apps.BooksConfig",
"movies.apps.MoviesConfig",
"music.apps.MusicConfig",
"games.apps.GamesConfig",
"collection.apps.CollectionConfig",
"upgrade_0_5.apps.Upgrade05Config",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",

View file

View file

@ -1,9 +0,0 @@
from django.contrib import admin
from .models import *
from simple_history.admin import SimpleHistoryAdmin
admin.site.register(Book, SimpleHistoryAdmin)
admin.site.register(BookMark)
admin.site.register(BookReview)
admin.site.register(BookTag)

View file

@ -1,10 +0,0 @@
from django.apps import AppConfig
class BooksConfig(AppConfig):
name = 'books'
def ready(self):
from common.index import Indexer
from .models import Book
Indexer.update_model_indexable(Book)

View file

@ -1,116 +0,0 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import Book, BookMark, BookReview, BookMarkStatusTranslation
from common.models import MarkStatusEnum
from common.forms import *
def BookMarkStatusTranslator(status):
return BookMarkStatusTranslation[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())
other_info = JSONField(required=False, label=_("其他信息"))
class Meta:
model = Book
fields = [
'id',
'title',
'source_site',
'source_url',
'isbn',
'author',
'pub_house',
'subtitle',
'translator',
'orig_title',
'language',
'pub_month',
'pub_year',
'binding',
'price',
'pages',
'cover',
'brief',
'contents',
'other_info',
]
labels = {
'title': _("书名"),
'isbn': _("ISBN"),
'author': _("作者"),
'pub_house': _("出版社"),
'subtitle': _("副标题"),
'translator': _("译者"),
'orig_title': _("原作名"),
'language': _("语言"),
'pub_month': _("出版月份"),
'pub_year': _("出版年份"),
'binding': _("装帧"),
'price': _("定价"),
'pages': _("页数"),
'cover': _("封面"),
'brief': _("简介"),
'contents': _("目录"),
'other_info': _("其他信息"),
}
widgets = {
'author': forms.TextInput(attrs={'placeholder': _("多个作者使用英文逗号分隔")}),
'translator': forms.TextInput(attrs={'placeholder': _("多个译者使用英文逗号分隔")}),
# 'cover': forms.FileInput(),
'cover': PreviewImageInput(),
}
def clean_isbn(self):
isbn = self.cleaned_data.get('isbn')
if isbn:
isbn = isbn.strip()
return isbn
class BookMarkForm(MarkForm):
STATUS_CHOICES = [(v, BookMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = BookMark
fields = [
'id',
'book',
'status',
'rating',
'text',
'visibility',
]
widgets = {
'book': forms.TextInput(attrs={"hidden": ""}),
}
class BookReviewForm(ReviewForm):
class Meta:
model = BookReview
fields = [
'id',
'book',
'title',
'content',
'visibility'
]
widgets = {
'book': forms.TextInput(attrs={"hidden": ""}),
}

View file

@ -1,200 +0,0 @@
from django.core.management.base import BaseCommand
from django.core.files.uploadedfile import SimpleUploadedFile
from django.conf import settings
from common.scraper import *
from books.models import Book
from books.forms import BookForm
import requests
import re
import filetype
from lxml import html
from PIL import Image
from io import BytesIO
class DoubanPatcherMixin:
@classmethod
def download_page(cls, url, headers):
url = cls.get_effective_url(url)
r = None
error = 'DoubanScrapper: error occured when downloading ' + url
content = None
def get(url, timeout):
nonlocal r
# print('Douban GET ' + url)
try:
r = requests.get(url, timeout=timeout)
except Exception as e:
r = requests.Response()
r.status_code = f"Exception when GET {url} {e}" + url
# print('Douban CODE ' + str(r.status_code))
return r
def check_content():
nonlocal r, error, content
content = None
if r.status_code == 200:
content = r.content.decode('utf-8')
if content.find('关于豆瓣') == -1:
# with open('/tmp/temp.html', 'w', encoding='utf-8') as fp:
# fp.write(content)
content = None
error = error + 'Content not authentic' # response is garbage
elif re.search('不存在[^<]+</title>', content, re.MULTILINE):
content = None
error = error + 'Not found or hidden by Douban'
else:
error = error + str(r.status_code)
def fix_wayback_links():
nonlocal content
# fix links
content = re.sub(r'href="http[^"]+http', r'href="http', content)
# https://img9.doubanio.com/view/subject/{l|m|s}/public/s1234.jpg
content = re.sub(r'src="[^"]+/(s\d+\.\w+)"',
r'src="https://img9.doubanio.com/view/subject/m/public/\1"', content)
# https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2681329386.jpg
# https://img9.doubanio.com/view/photo/{l|m|s}/public/p1234.webp
content = re.sub(r'src="[^"]+/(p\d+\.\w+)"',
r'src="https://img9.doubanio.com/view/photo/m/public/\1"', content)
# Wayback Machine: get latest available
def wayback():
nonlocal r, error, content
error = error + '\nWayback: '
get('http://archive.org/wayback/available?url=' + url, 10)
if r.status_code == 200:
w = r.json()
if w['archived_snapshots'] and w['archived_snapshots']['closest']:
get(w['archived_snapshots']['closest']['url'], 10)
check_content()
if content is not None:
fix_wayback_links()
else:
error = error + 'No snapshot available'
else:
error = error + str(r.status_code)
# Wayback Machine: guess via CDX API
def wayback_cdx():
nonlocal r, error, content
error = error + '\nWayback: '
get('http://web.archive.org/cdx/search/cdx?url=' + url, 10)
if r.status_code == 200:
dates = re.findall(r'[^\s]+\s+(\d+)\s+[^\s]+\s+[^\s]+\s+\d+\s+[^\s]+\s+\d{5,}',
r.content.decode('utf-8'))
# assume snapshots whose size >9999 contain real content, use the latest one of them
if len(dates) > 0:
get('http://web.archive.org/web/' + dates[-1] + '/' + url, 10)
check_content()
if content is not None:
fix_wayback_links()
else:
error = error + 'No snapshot available'
else:
error = error + str(r.status_code)
def latest():
nonlocal r, error, content
if settings.SCRAPESTACK_KEY is None:
error = error + '\nDirect: '
get(url, 60)
else:
error = error + '\nScrapeStack: '
get(f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}', 60)
check_content()
wayback_cdx()
if content is None:
latest()
if content is None:
logger.error(error)
content = '<html />'
return html.fromstring(content)
@classmethod
def download_image(cls, url, item_url=None):
if url is None:
logger.error(f"Douban: no image url for {item_url}")
return None, None
raw_img = None
ext = None
dl_url = url
if settings.SCRAPESTACK_KEY is not None:
dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
try:
img_response = requests.get(dl_url, timeout=90)
if img_response.status_code == 200:
raw_img = img_response.content
img = Image.open(BytesIO(raw_img))
img.load() # corrupted image will trigger exception
content_type = img_response.headers.get('Content-Type')
ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension
else:
logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}")
# raise RuntimeError(f"Douban: download image failed {img_response.status_code} {dl_url}")
except Exception as e:
raw_img = None
ext = None
logger.error(f"Douban: download image failed {e} {dl_url} {item_url}")
if raw_img is None and settings.SCRAPESTACK_KEY is not None:
try:
img_response = requests.get(dl_url, timeout=90)
if img_response.status_code == 200:
raw_img = img_response.content
img = Image.open(BytesIO(raw_img))
img.load() # corrupted image will trigger exception
content_type = img_response.headers.get('Content-Type')
ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension
else:
logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}")
except Exception as e:
raw_img = None
ext = None
logger.error(f"Douban: download image failed {e} {dl_url} {item_url}")
return raw_img, ext
class DoubanBookPatcher(DoubanPatcherMixin, AbstractScraper):
site_name = SourceSiteEnum.DOUBAN.value
host = 'book.douban.com'
data_class = Book
form_class = BookForm
regex = re.compile(r"https://book\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
headers['Host'] = self.host
content = self.download_page(url, headers)
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, ext = self.download_image(img_url, url)
return raw_img, ext
class Command(BaseCommand):
help = 'fix cover image'
def add_arguments(self, parser):
parser.add_argument('threadId', type=int, help='% 8')
def handle(self, *args, **options):
t = int(options['threadId'])
for m in Book.objects.filter(cover='book/default.svg', source_site='douban'):
if m.id % 8 == t:
self.stdout.write(f'Re-fetching {m.source_url}')
try:
raw_img, img_ext = DoubanBookPatcher.scrape(m.source_url)
if img_ext is not None:
m.cover = SimpleUploadedFile('temp.' + img_ext, raw_img)
m.save()
self.stdout.write(self.style.SUCCESS(f'Saved {m.source_url}'))
else:
self.stdout.write(self.style.ERROR(f'Skipped {m.source_url}'))
except Exception as e:
print(e)

View file

@ -1,188 +0,0 @@
import django.contrib.postgres.fields as postgres
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.shortcuts import reverse
from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
from common.utils import GenerateDateUUIDMediaFilePath
from django.conf import settings
from django.db.models import Q
from simple_history.models import HistoricalRecords
BookMarkStatusTranslation = {
MarkStatusEnum.DO.value: _("在读"),
MarkStatusEnum.WISH.value: _("想读"),
MarkStatusEnum.COLLECT.value: _("读过")
}
def book_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.BOOK_MEDIA_PATH_ROOT)
class Book(Entity):
# widely recognized name, usually in Chinese
title = models.CharField(_("title"), max_length=500)
subtitle = models.CharField(
_("subtitle"), blank=True, default='', max_length=500)
# original name, for books in foreign language
orig_title = models.CharField(
_("original title"), blank=True, default='', max_length=500)
author = postgres.ArrayField(
models.CharField(_("author"), blank=True, default='', max_length=200),
null=True,
blank=True,
default=list,
)
translator = postgres.ArrayField(
models.CharField(_("translator"), blank=True,
default='', max_length=200),
null=True,
blank=True,
default=list,
)
language = models.CharField(
_("language"), blank=True, default='', max_length=50)
pub_house = models.CharField(
_("publishing house"), blank=True, default='', max_length=200)
pub_year = models.IntegerField(_("published year"), null=True, blank=True)
pub_month = models.IntegerField(
_("published month"), null=True, blank=True)
binding = models.CharField(
_("binding"), blank=True, default='', max_length=200)
# 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, null=False,
max_length=20, db_index=True, default='')
# to store previously scrapped data
cover = models.ImageField(_("cover picture"), upload_to=book_cover_path,
default=settings.DEFAULT_BOOK_IMAGE, blank=True)
contents = models.TextField(blank=True, default="")
history = HistoricalRecords()
class Meta:
constraints = [
models.CheckConstraint(check=models.Q(
pub_year__gte=0), name='pub_year_lowerbound'),
models.CheckConstraint(check=models.Q(
pub_month__lte=12), name='pub_month_upperbound'),
models.CheckConstraint(check=models.Q(
pub_month__gte=1), name='pub_month_lowerbound'),
]
def __str__(self):
return self.title
def get_json(self):
r = {
'subtitle': self.subtitle,
'original_title': self.orig_title,
'author': self.author,
'translator': self.translator,
'publisher': self.pub_house,
'publish_year': self.pub_year,
'publish_month': self.pub_month,
'language': self.language,
'isbn': self.isbn,
}
r.update(super().get_json())
return r
def get_absolute_url(self):
return reverse("books:retrieve", args=[self.id])
@property
def wish_url(self):
return reverse("books:wish", args=[self.id])
def get_tags_manager(self):
return self.book_tags
def get_related_books(self):
qs = Q(orig_title=self.title)
if self.isbn:
qs = qs | Q(isbn=self.isbn)
if self.orig_title:
qs = qs | Q(title=self.orig_title)
qs = qs | Q(orig_title=self.orig_title)
qs = qs & ~Q(id=self.id)
return Book.objects.filter(qs)
def get_identicals(self):
qs = Q(orig_title=self.title)
if self.isbn:
qs = Q(isbn=self.isbn)
# qs = qs & ~Q(id=self.id)
return Book.objects.filter(qs)
else:
return [self] # Book.objects.filter(id=self.id)
@property
def year(self):
return self.pub_year
@property
def verbose_category_name(self):
return _("书籍")
@property
def mark_class(self):
return BookMark
@property
def tag_class(self):
return BookTag
class BookMark(Mark):
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")
]
@property
def translated_status(self):
return BookMarkStatusTranslation[self.status]
class BookReview(Review):
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")
]
@property
def url(self):
return reverse("books:retrieve_review", args=[self.id])
@property
def item(self):
return self.book
class BookTag(Tag):
book = models.ForeignKey(
Book, on_delete=models.CASCADE, related_name='book_tags', null=True)
mark = models.ForeignKey(
BookMark, on_delete=models.CASCADE, related_name='bookmark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_bookmark_tag")
]
@property
def item(self):
return self.book

View file

@ -1,89 +0,0 @@
{% 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>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
{% if is_update and form.source_site.value != 'in-site' %}
<div style="float:right;padding-left:16px">
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
<div class="action-panel__button-group">
<form method="post" action="{% url 'books:rescrape' form.id.value %}">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
</form>
</div>
</div>
</div>
</div>
{% endif %}
<div class="single-section-wrapper" id="main">
{% comment %} <a href="{% url 'books:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{{ form }}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -1,110 +0,0 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/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.min.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 'books:retrieve' book.id %}">
<img src="{{ book.cover|thumb:'normal' }}" 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 'books:retrieve' book.id %}">{{ book.title }}</a>
<a href="{{ book.source_url }}">
<span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
</a>
</h5>
<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 %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ book.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.book }}
<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.visibility.label }}{{ form.visibility }}
</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>
<script>
</script>
</body>
</html>

View file

@ -1,99 +0,0 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '删除图书' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这本书吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'books:retrieve' book.id %}">
<img src="{{ book.cover|thumb:'normal' }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'books:retrieve' book.id %}">
<h5 class="entity-card__title">
{{ book.title }}
<a href="{{ book.source_url }}">
<span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
</a>
</h5>
</a>
{% if book.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ book.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if book.last_editor %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' book.last_editor.mastodon_username %}">
<span>{{ book.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ book.edited_time }}</div>
{% if book.book_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ book.book_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if book.book_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ book.book_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</div>
</div>
<div class="dividing-line"></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>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,102 +0,0 @@
{% 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>{{ site_name }} - {% trans '删除评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="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.visibility > 0 %}
<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.mastodon_username %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></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>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -1,417 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}书 - {{ book.title }}">
<meta property="og:type" content="book">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ book.cover.url }}">
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:description" content="{{ book.brief }}">
{% if book.author %}
<meta property="og:book:author" content="{% for author in book.author %}{{ author }}{% if not forloop.last %},{% endif %}{% endfor %}">
{% endif %}
{% if book.isbn %}
<meta property="og:book:isbn" content="{{ book.isbn }}">
{% endif %}
<title>{{ site_name }} - {% trans '书籍详情' %} | {{ book.title }}</title>
{% include "partial/_common_libs.html" with jquery=1 %}
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/detail.js' %}"></script>
</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">
<a href="{{ book.cover.url }}" class="entity-detail__img-origin" target="_blank" title="{% trans '查看原图' %}">
<img src="{{ book.cover|thumb:'normal' }}" class="entity-detail__img" alt="{{ book.title }}">
</a>
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ book.title }}
<a href="{{ book.source_url }}">
<span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
</a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if book.rating and book.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ book.rating }} </span>
<small>({{ book.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if book.isbn %}{% trans 'ISBN' %}{{ book.isbn }}{% endif %}</div>
<div>{% if book.author %}{% trans '作者:' %}
{% for author in book.author %}
<span>{{ author }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}</div>
<div>{% if book.subtitle %}{% trans '副标题:' %}{{ book.subtitle }}{% endif %}</div>
<div>{% if book.translator %}{% trans '译者:' %}
{% for translator in book.translator %}
<span>{{ translator }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if book.orig_title %}{% trans '原作名:' %}{{ book.orig_title }}{% endif %}</div>
<div>{% if book.language %}{% trans '语言:' %}{{ book.language }}{% endif %}</div>
<div>{%if book.pub_year %}{% trans '出版时间:' %}{{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{ book.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
</div>
<div class="entity-detail__fields">
<div>{% if book.binding %}{% trans '装帧:' %}{{ book.binding }}{% endif %}</div>
<div>{% if book.price %}{% trans '定价:' %}{{ book.price }}{% endif %}</div>
<div>{% if book.pages %}{% trans '页数:' %}{{ book.pages }}{% endif %}</div>
{% if book.other_info %}
{% for k, v in book.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if book.last_editor and book.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.mastodon_username %}">{{ book.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'books:update' book.id %}">{% trans '编辑这本书' %}</a>
{% if user.is_staff %}
/<a href="{% url 'books:delete' book.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in book_tag_list %}
{% for k, v in tag_dict.items %}
{% if k == 'content' %}
<span class="tag-collection__tag">
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
</span>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</div>
</div>
<div class="dividing-line"></div>
{% if book.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<p class="entity-desc__content">{{ book.brief | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
{% if book.contents %}
<div class="entity-desc" id="contents">
<h5 class="entity-desc__title">{% trans '目录' %}</h5>
<p class="entity-desc__content">{{ book.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>
<a href="{% url 'books:retrieve_mark_list' book.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
<a href="{% url 'books:retrieve_mark_list' book.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
{% include "partial/mark_list.html" with mark_list=mark_list current_item=book %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这本书的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'books:retrieve_review_list' book.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.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
{% if others_review.book != book %}
<span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'books:retrieve' others_review.book.id %}">{{ others_review.book.get_source_site_display }}</a></span>
{% endif %}
<span class="entity-reviews__review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
<span>{{ others_review.get_plain_content | truncate:100 }}</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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'books: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.created_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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">
<a href="{% url 'books:update_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'books:delete_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'books: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 'books:create_review' book.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
{% if book.get_related_books.count > 0 %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关书目' %}</div>
<div >
{% for b in book.get_related_books %}
<p>
<a href="{% url 'books:retrieve' b.id %}">{{ b.title }}</a>
<small>({{ b.pub_house }} {{ b.pub_year }})</small>
<span class="source-label source-label__{{ b.source_site }}">{{ b.get_source_site_display }}</span>
</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if book.isbn %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '借阅或购买' %}</div>
<div class="action-panel__button-group">
<a class="action-panel__button" target="_blank" href="https://www.worldcat.org/isbn/{{ book.isbn }}">{% trans 'WorldCat' %}</a>
<a class="action-panel__button" target="_blank" href="https://openlibrary.org/search?isbn={{ book.isbn }}">{% trans 'Open Library' %}</a>
</div>
</div>
</div>
{% endif %}
{% if collection_list %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关收藏单' %}</div>
<div >
{% for c in collection_list %}
<p>
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
</p>
{% endfor %}
<div class="action-panel__button-group action-panel__button-group--center">
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'book' book.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
</div>
</div>
</div>
</div>
{% endif %}
</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 'books:create_update_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.book }}
{% 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.visibility.label }}:
{{ mark_form.visibility }}</span>
</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

@ -1,111 +0,0 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ book.title }}{% trans '的标记' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>{% trans ' 的标记' %}
</h5>
{% include "partial/mark_list.html" with mark_list=marks current_item=book %}
</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 'books:retrieve' book.id %}"><img src="{{ book.cover|thumb:'normal' }}" alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>
<a href="{{ book.source_url }}">
<span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
</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="entity-card__rating-star rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ book.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,124 +0,0 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}书评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ book.cover|thumb:'normal' }}">
<title>{{ site_name }}{% trans '书评' %} - {{ review.title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link" href="{% url 'books:update_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link" href="{% url 'books:delete_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
{% csrf_token %}
</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 'books:retrieve' book.id %}"><img src="{{ book.cover|thumb:'normal' }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title">
<a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>
<a href="{{ book.source_url }}">
<span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
</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="entity-card__rating-star rating-star"
data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ book.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -1,131 +0,0 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ book.title }}{% trans '的评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>{% trans ' 的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
{% if review.book != book %}
<span class="entity-reviews__review-time source-label"><a href="{% url 'books:retrieve' review.book.id %}" class="entity-reviews__review-time">{{ review.book.get_source_site_display }}</a></span>
{% endif %}
<span href="{% url 'books:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'books:retrieve_review' review.id %}">{{ review.title }}</a></span>
</li>
{% empty %}
<div>{% trans '无结果' %}</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if reviews.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ reviews.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in reviews.pagination.page_range %}
{% if page == reviews.pagination.current_page %}
<a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
{% else %}
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
{% endif %}
{% endfor %}
{% if reviews.pagination.has_next %}
<a href="?page={{ reviews.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'books:retrieve' book.id %}"><img src="{{ book.cover|thumb:'normal' }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>
<a href="{{ book.source_url }}">
<span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
</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="entity-card__rating-star rating-star"
data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ book.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,111 +0,0 @@
{% 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>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.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" src="https://search.douban.com/book/subject_search{% if q %}?search_text={{ q }}{% endif %}" frameborder="0"></iframe>
<div class="dividing-line"></div>
<div id="parser">
<label style="font-size: small;">{% trans '解析器:' %}</label>
<textarea name="parser" id="" cols="30" rows="40" placeholder="复制信息粘贴至此自动解析,例:
作者: [法] 雨果
出版社: 人民文学出版社
原作名: Les Misérables
译者: 李丹 / 方于
出版年: 2015-6
页数: 1435
定价: 110.00元
装帧: 精装
丛书: 名著名译丛书
ISBN: 9787020104345
"></textarea>
</div>
<div class="dividing-line"></div>
<div id="scrapeForm">
<form action="{% url 'books:create' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{{ form }}
</form>
<a href="#" 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 'books:click_to_scrape' %}" method="post">
{% csrf_token %}
<input type="text" name="url" required placeholder="https://book.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>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,23 +0,0 @@
from django.urls import path, re_path
from .views import *
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('rescrape/<int:id>/', rescrape, name='rescrape'),
path('mark/', create_update_mark, name='create_update_mark'),
path('wish/<int:id>/', wish, name='wish'),
re_path('(?P<book_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', 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'),
path('click_to_scrape/', click_to_scrape, name='click_to_scrape'),
]

View file

@ -1,583 +0,0 @@
import logging
from django.shortcuts import render, get_object_or_404, redirect, reverse
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse
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 mastodon import mastodon_request_included
from mastodon.models import MastodonApplication
from mastodon.api import share_mark, share_review
from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
from common.models import SourceSiteEnum
from .models import *
from .forms import *
from .forms import BookMarkStatusTranslator
from django.conf import settings
from collection.models import CollectionItem
from common.scraper import get_scraper_by_url, get_normalized_url
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 = BookForm()
return render(
request,
'books/create_update.html',
{
'form': form,
'title': _('添加书籍'),
'submit_url': reverse("books:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
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
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
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"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
else:
return redirect(reverse("users:login"))
else:
return HttpResponseBadRequest()
@login_required
def rescrape(request, id):
if request.method != 'POST':
return HttpResponseBadRequest()
item = get_object_or_404(Book, pk=id)
url = get_normalized_url(item.source_url)
scraper = get_scraper_by_url(url)
scraper.scrape(url)
form = scraper.save(request_user=request.user, instance=item)
return redirect(reverse("books:retrieve", args=[form.instance.id]))
@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,
'is_update': True,
'title': _('修改书籍'),
'submit_url': reverse("books:update", args=[book.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
book = get_object_or_404(Book, pk=id)
form = BookForm(request.POST, request.FILES, instance=book)
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
else:
return render(
request,
'books/create_update.html',
{
'form': form,
'is_update': True,
'title': _('修改书籍'),
'submit_url': reverse("books:update", args=[book.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
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
mark_tags = None
review = None
# retreive tags
book_tag_list = book.book_tags.values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER]
# retrieve user mark and initialize mark form
try:
if request.user.is_authenticated:
mark = BookMark.objects.get(owner=request.user, book=book)
except ObjectDoesNotExist:
mark = None
if mark:
mark_tags = mark.bookmark_tags.all()
mark.get_status_display = BookMarkStatusTranslator(mark.status)
mark_form = BookMarkForm(instance=mark, initial={
'tags': mark_tags
})
else:
mark_form = BookMarkForm(initial={
'book': book,
'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
'tags': mark_tags
})
# retrieve user review
try:
if request.user.is_authenticated:
review = BookReview.objects.get(owner=request.user, book=book)
except ObjectDoesNotExist:
review = None
# retrieve other related reviews and marks
if request.user.is_anonymous:
# hide all marks and reviews for anonymous user
mark_list = None
review_list = None
mark_list_more = None
review_list_more = None
else:
mark_list = BookMark.get_available_for_identicals(book, request.user)
review_list = BookReview.get_available_for_identicals(book, request.user)
mark_list_more = True if len(mark_list) > MARK_NUMBER else False
mark_list = mark_list[:MARK_NUMBER]
for m in mark_list:
m.get_status_display = BookMarkStatusTranslator(m.status)
review_list_more = True if len(
review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER]
all_collections = CollectionItem.objects.filter(book=book).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# 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,
'book_tag_list': book_tag_list,
'mark_tags': mark_tags,
'collection_list': collection_list,
}
)
else:
logger.warning('non-GET method at /book/<id>')
return HttpResponseBadRequest()
@permission_required('books.delete_book')
@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:home"))
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
old_tags = None
if not pk:
book_id = request.POST.get('book')
mark = BookMark.objects.filter(book_id=book_id, owner=request.user).first()
if mark:
pk = mark.id
if pk:
mark = get_object_or_404(BookMark, pk=pk)
if request.user != mark.owner:
return HttpResponseBadRequest()
old_rating = mark.rating
old_tags = mark.bookmark_tags.all()
if mark.status != request.POST.get('status'):
mark.created_time = timezone.now()
# update
form = BookMarkForm(request.POST, instance=mark)
else:
# create
form = BookMarkForm(request.POST)
if form.is_valid():
if form.instance.status == MarkStatusEnum.WISH.value or form.instance.rating == 0:
form.instance.rating = None
form.cleaned_data['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()
# update tags
if old_tags:
for tag in old_tags:
tag.delete()
if form.cleaned_data['tags']:
for tag in form.cleaned_data['tags']:
BookTag.objects.create(
content=tag,
book=book,
mark=form.instance
)
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
if form.cleaned_data['share_to_mastodon']:
if not share_mark(form.instance):
return go_relogin(request)
else:
return HttpResponseBadRequest(f"invalid form data {form.errors}")
return redirect(reverse("books:retrieve", args=[form.instance.book.id]))
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def wish(request, id):
if request.method == 'POST':
book = get_object_or_404(Book, pk=id)
params = {
'owner': request.user,
'status': MarkStatusEnum.WISH,
'visibility': request.user.preference.default_visibility,
'book': book,
}
try:
BookMark.objects.create(**params)
except Exception:
pass
return HttpResponse("✔️")
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def retrieve_mark_list(request, book_id, following_only=False):
if request.method == 'GET':
book = get_object_or_404(Book, pk=book_id)
queryset = BookMark.get_available_for_identicals(book, request.user, following_only=following_only)
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 = 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)
if request.user != mark.owner:
return HttpResponseBadRequest()
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 not share_review(form.instance):
return go_relogin(request)
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):
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 not share_review(form.instance):
return go_relogin(request)
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
def retrieve_review(request, id):
if request.method == 'GET':
review = get_object_or_404(BookReview, pk=id)
if not review.is_visible_to(request.user):
msg = _("你没有访问这个页面的权限😥")
return render(
request,
'common/error.html',
{
'msg': msg,
}
)
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_for_identicals(book, request.user)
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,
'books/review_list.html',
{
'reviews': reviews,
'book': book,
}
)
else:
return HttpResponseBadRequest()
@login_required
def scrape(request):
if request.method == 'GET':
keywords = request.GET.get('q')
form = BookForm()
return render(
request,
'books/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:
return jump_or_scrape(request, url)
else:
return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()

View file

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,6 +0,0 @@
from django.apps import AppConfig
class CollectionConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'collection'

View file

@ -1,45 +0,0 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import Collection
from common.forms import *
COLLABORATIVE_CHOICES = [
(0, _("仅限创建者")),
(1, _("创建者及其互关用户")),
]
class CollectionForm(forms.ModelForm):
# id = forms.IntegerField(required=False, widget=forms.HiddenInput())
title = forms.CharField(label=_("标题"))
description = MarkdownxFormField(label=_("详细介绍 (Markdown)"))
# share_to_mastodon = forms.BooleanField(label=_("分享到联邦网络"), initial=True, required=False)
visibility = forms.TypedChoiceField(
label=_("可见性"),
initial=0,
coerce=int,
choices=VISIBILITY_CHOICES,
widget=forms.RadioSelect
)
collaborative = forms.TypedChoiceField(
label=_("协作整理权限"),
initial=0,
coerce=int,
choices=COLLABORATIVE_CHOICES,
widget=forms.RadioSelect
)
class Meta:
model = Collection
fields = [
'title',
'description',
'cover',
'visibility',
'collaborative',
]
widgets = {
'cover': PreviewImageInput(),
}

View file

@ -1,128 +0,0 @@
from django.db import models
from markdown import markdown
from common.models import UserOwnedEntity
from movies.models import Movie
from books.models import Book
from music.models import Song, Album
from games.models import Game
from markdownx.models import MarkdownxField
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
from common.models import RE_HTML_TAG
from django.shortcuts import reverse
def collection_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.COLLECTION_MEDIA_PATH_ROOT)
class Collection(UserOwnedEntity):
title = models.CharField(max_length=200)
description = MarkdownxField()
cover = models.ImageField(_("封面"), upload_to=collection_cover_path, default=settings.DEFAULT_COLLECTION_IMAGE, blank=True)
collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers
def __str__(self):
return f"Collection({self.id} {self.owner} {self.title})"
@property
def translated_status(self):
return '创建了收藏单'
@property
def collectionitem_list(self):
return sorted(list(self.collectionitem_set.all()), key=lambda i: i.position)
@property
def item_list(self):
return map(lambda i: i.item, self.collectionitem_list)
@property
def plain_description(self):
html = markdown(self.description)
return RE_HTML_TAG.sub(' ', html)
def has_item(self, item):
return len(list(filter(lambda i: i.item == item, self.collectionitem_list))) > 0
def append_item(self, item, comment=""):
cl = self.collectionitem_list
if item is None or self.has_item(item):
return None
else:
i = CollectionItem(collection=self, position=cl[-1].position + 1 if len(cl) else 1, comment=comment)
i.set_item(item)
i.save()
return i
@property
def item(self):
return self
@property
def mark_class(self):
return CollectionMark
@property
def url(self):
return reverse("collection:retrieve", args=[self.id])
@property
def wish_url(self):
return reverse("collection:wish", args=[self.id])
def is_editable_by(self, viewer):
if viewer.is_staff or viewer.is_superuser or viewer == self.owner:
return True
elif self.collaborative == 1 and viewer.is_following(self.owner) and viewer.is_followed_by(self.owner):
return True
else:
return False
class CollectionItem(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, null=True)
album = models.ForeignKey(Album, on_delete=models.CASCADE, null=True)
song = models.ForeignKey(Song, on_delete=models.CASCADE, null=True)
book = models.ForeignKey(Book, on_delete=models.CASCADE, null=True)
game = models.ForeignKey(Game, on_delete=models.CASCADE, null=True)
collection = models.ForeignKey(Collection, on_delete=models.CASCADE)
position = models.PositiveIntegerField()
comment = models.TextField(_("备注"), default='')
@property
def item(self):
items = list(filter(lambda i: i is not None, [self.movie, self.book, self.album, self.song, self.game]))
return items[0] if len(items) > 0 else None
# @item.setter
def set_item(self, new_item):
old_item = self.item
if old_item == new_item:
return
if old_item is not None:
self.movie = None
self.book = None
self.album = None
self.song = None
self.game = None
setattr(self, new_item.__class__.__name__.lower(), new_item)
class CollectionMark(UserOwnedEntity):
collection = models.ForeignKey(
Collection, on_delete=models.CASCADE, related_name='collection_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'collection'], name="unique_collection_mark")
]
def __str__(self):
return f"CollectionMark({self.id} {self.owner} {self.collection})"
@property
def translated_status(self):
return '关注了收藏单'

View file

@ -1,45 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
<div class="modal-underlay" _="on click trigger closeModal"></div>
<div class="modal-content">
<div class="add-to-list-modal__head">
<span class="add-to-list-modal__title">{% trans '添加到收藏单' %}</span>
<span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
<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="add-to-list-modal__body">
<form action="/collections/add_to_list/{{ type }}/{{ id }}/" method="post">
{% csrf_token %}
<select name="collection_id">
{% for collection in collections %}
<option value="{{ collection.id }}">{{ collection.title }}{% if collection.visibility > 0 %}🔒{% endif %}</option>
{% endfor %}
<option value="0">新建收藏单</option>
</select>
<div>
<textarea type="text" name="comment" placeholder="条目备注"></textarea>
</div>
<div class="add-to-list-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '提交' %}">
</div>
</form>
</div>
</div>
</div>

View file

@ -1,71 +0,0 @@
{% 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>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<style type="text/css">
#id_collaborative li, #id_visibility li {display: inline-block !important;}
</style>
</head>
<body>
<div id="page-wrapper">
{% include "partial/_navbar.html" %}
<div id="content-wrapper">
<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>
{{ form.media }}
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -1,117 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
<meta property="og:description" content="{{ collection.description }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ collection.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
<title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.4/htmx.min.js"></script>
</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">
确认删除收藏单「{{ collection.title }}」吗?
</h5>
{% if collection.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
<span class="review-head__time">{{ collection.edited_time }}</span>
</div>
<div class="review-head__actions">
</div>
</div>
<div id="rawContent">
{{ form.description }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'collection:delete' collection.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 class="dividing-line"></div> -->
<!-- <div class="entity-card__img-wrapper" style="text-align: center;">
<img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
</div> -->
</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 'collection:retrieve' collection.id %}">
<img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
</a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title">
<a href="{% url 'collection:retrieve' collection.id %}">
{{ collection.title }}
</a>
</h5>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
</script>
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
})
</script>
</body>
</html>

View file

@ -1,150 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
<meta property="og:description" content="{{ collection.description }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ collection.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
<title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
{% include "partial/_common_libs.html" with jquery=1 %}
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
</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">
{{ collection.title }}
</h5>
{% if collection.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
<span class="review-head__time">{{ collection.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == collection.owner %}
<a class="review-head__action-link" href="{% url 'collection:update' collection.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link" href="{% url 'collection:delete' collection.id %}">{% trans '删除' %}</a>
{% elif editable %}
<span class="review-head__time">可协作整理</span>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<!-- <div class="entity-card__img-wrapper" style="text-align: center;">
<img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
</div> -->
<div id="rawContent">
{{ form.description }}
</div>
{{ form.media }}
</div>
<div class="entity-list" hx-get="{% url 'collection:retrieve_entity_list' collection.id %}" hx-trigger="load">
</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 'collection:retrieve' collection.id %}">
<img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
</a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title">
<a href="{% url 'collection:retrieve' collection.id %}">
{{ collection.title }}
</a>
</h5>
{% if follower_count %}
被 {{ follower_count }} 人关注
{% endif %}
</div>
</div>
</div>
{% if request.user != collection.owner %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__button-group action-panel__button-group--center">
{% if following %}
<form action="{% url 'collection:unfollow' collection.id %}" method="post">
{% csrf_token %}
<button class="action-panel__button">{% trans '取消关注' %}</button>
</form>
{% else %}
<form action="{% url 'collection:follow' collection.id %}" method="post">
{% csrf_token %}
<button class="action-panel__button">{% trans '关注' %}</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__button-group action-panel__button-group--center">
<form>
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:share' collection.id %}" hx-target="body" hx-swap="beforeend">{% trans '分享到联邦网络' %}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
</script>
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
})
</script>
</body>
</html>

View file

@ -1,5 +0,0 @@
<form hx-post="{% url 'collection:update_item_comment' collection.id collectionitem.id %}">
<input name="comment" value="{{ collectionitem.comment }}">
<input type="submit" style="width:unset;" value="修改">
<button style="width:unset;" hx-get="{% url 'collection:show_item_comment' collection.id collectionitem.id %}">取消</button>
</form>

View file

@ -1,21 +0,0 @@
{% load thumb %}
{% load i18n %}
{% load l10n %}
<ul class="entity-list__entities">
{% for collectionitem in collection.collectionitem_list %}
{% if collectionitem.item is not None %}
{% include "partial/list_item.html" with item=collectionitem.item %}
{% endif %}
{% empty %}
{% endfor %}
{% if editable %}
<li>
<form hx-target=".entity-list" hx-post="{% url 'collection:append_item' form.instance.id %}" method="POST">
{% csrf_token %}
<input type="url" name="url" placeholder="{{ request.scheme }}://{{ request.get_host }}/movies/1/" style="min-width:24rem" required>
<input type="text" name="comment" placeholder="{% trans '备注' %}" style="min-width:24rem">
<input class="button" type="submit" value="{% trans '添加' %}" >
</form>
</li>
{% endif %}
</ul>

View file

@ -1,99 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</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">
{{ title }}
</h5>
<ul class="entity-reviews__review-list">
{% for collection in collections %}
<li class="entity-reviews__review entity-reviews__review--wider">
<img src="{{ collection.cover|thumb:'normal' }}" style="width:40px; float:right"class="entity-card__img">
<span class="entity-reviews__review-title"><a href="{% url 'collection:retrieve' collection.id %}">{{ collection.title }}</a></span>
<span class="entity-reviews__review-time">{{ collection.edited_time }}</span>
{% if collection.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
</li>
{% empty %}
<div>{% trans '无结果' %}</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if collections.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ collections.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in collections.pagination.page_range %}
{% if page == collections.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 collections.pagination.has_next %}
<a href="?page={{ collections.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ collections.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,56 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
<div class="modal-underlay" _="on click trigger closeModal"></div>
<div class="modal-content">
<div class="add-to-list-modal__head">
<span class="add-to-list-modal__title">{% trans '分享收藏单' %}</span>
<span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
<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="add-to-list-modal__body">
<form action="/collections/share/{{ id }}/" method="post">
{% csrf_token %}
<div>
<label for="id_visibility_0">分享可见性(不同于收藏单本身的权限):</label>
<ul id="id_visibility">
<li><label for="id_visibility_0"><input type="radio" name="visibility" value="0" required="" id="id_visibility_0" {% if visibility == 0 %}checked{% endif %}>
公开</label>
</li>
<li><label for="id_visibility_1"><input type="radio" name="visibility" value="1" required="" id="id_visibility_1" {% if visibility == 1 %}checked{% endif %}>
仅关注者</label>
</li>
<li><label for="id_visibility_2"><input type="radio" name="visibility" value="2" required="" id="id_visibility_2" {% if visibility == 2 %}checked{% endif %}>
仅自己</label>
</li>
</ul>
</div>
<div>
<textarea type="text" name="comment" placeholder="分享附言"></textarea>
</div>
<div class="add-to-list-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '提交' %}">
</div>
</form>
</div>
</div>
</div>

View file

@ -1,4 +0,0 @@
{{ collectionitem.comment }}
{% if editable %}
<a class="action-icon" hx-get="{% url 'collection:update_item_comment' collection.id collectionitem.id %}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19,20H5a1,1,0,0,0,0,2H19a1,1,0,0,0,0-2Z"/><path d="M5,18h.09l4.17-.38a2,2,0,0,0,1.21-.57l9-9a1.92,1.92,0,0,0-.07-2.71h0L16.66,2.6A2,2,0,0,0,14,2.53l-9,9a2,2,0,0,0-.57,1.21L4,16.91a1,1,0,0,0,.29.8A1,1,0,0,0,5,18ZM15.27,4,18,6.73,16,8.68,13.32,6Zm-8.9,8.91L12,7.32l2.7,2.7-5.6,5.6-3,.28Z"/></g></svg></a>
{% endif %}

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,27 +0,0 @@
from django.urls import path, re_path
from .views import *
app_name = 'collection'
urlpatterns = [
path('mine/', list, name='list'),
path('create/', create, name='create'),
path('<int:id>/', retrieve, name='retrieve'),
path('<int:id>/entity_list', retrieve_entity_list, name='retrieve_entity_list'),
path('update/<int:id>/', update, name='update'),
path('delete/<int:id>/', delete, name='delete'),
path('follow/<int:id>/', follow, name='follow'),
path('unfollow/<int:id>/', unfollow, name='unfollow'),
path('<int:id>/append_item/', append_item, name='append_item'),
path('<int:id>/delete_item/<int:item_id>', delete_item, name='delete_item'),
path('<int:id>/move_up_item/<int:item_id>', move_up_item, name='move_up_item'),
path('<int:id>/move_down_item/<int:item_id>', move_down_item, name='move_down_item'),
path('<int:id>/update_item_comment/<int:item_id>', update_item_comment, name='update_item_comment'),
path('<int:id>/show_item_comment/<int:item_id>', show_item_comment, name='show_item_comment'),
path('with/<str:type>/<int:id>/', list_with, name='list_with'),
path('add_to_list/<str:type>/<int:id>/', add_to_list, name='add_to_list'),
path('share/<int:id>/', share, name='share'),
path('follow2/<int:id>/', wish, name='wish'),
# TODO: tag
]

View file

@ -1,432 +0,0 @@
import logging
from django.shortcuts import render, get_object_or_404, redirect, reverse
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse
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 mastodon import mastodon_request_included
from mastodon.models import MastodonApplication
from mastodon.api import share_collection
from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
from common.models import SourceSiteEnum
from .models import *
from .forms import *
from django.conf import settings
import re
from users.models import User
from django.http import HttpResponseRedirect
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
class HTTPResponseHXRedirect(HttpResponseRedirect):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self['HX-Redirect'] = self['Location']
status_code = 200
# public data
###########################
@login_required
def create(request):
if request.method == 'GET':
form = CollectionForm()
return render(
request,
'create_update.html',
{
'form': form,
'title': _('添加收藏单'),
'submit_url': reverse("collection:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
if request.user.is_authenticated:
# only local user can alter public data
form = CollectionForm(request.POST, request.FILES)
form.instance.owner = request.user
if form.is_valid():
form.instance.last_editor = request.user
try:
with transaction.atomic():
form.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
return redirect(reverse("collection:retrieve", args=[form.instance.id]))
else:
return render(
request,
'create_update.html',
{
'form': form,
'title': _('添加收藏单'),
'submit_url': reverse("collection:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
else:
return redirect(reverse("users:login"))
else:
return HttpResponseBadRequest()
@login_required
def update(request, id):
page_title = _("修改收藏单")
collection = get_object_or_404(Collection, pk=id)
if not collection.is_visible_to(request.user):
raise PermissionDenied()
if request.method == 'GET':
form = CollectionForm(instance=collection)
return render(
request,
'create_update.html',
{
'form': form,
'is_update': True,
'title': page_title,
'submit_url': reverse("collection:update", args=[collection.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
form = CollectionForm(request.POST, request.FILES, instance=collection)
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
try:
with transaction.atomic():
form.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
else:
return render(
request,
'create_update.html',
{
'form': form,
'is_update': True,
'title': page_title,
'submit_url': reverse("collection:update", args=[collection.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
return redirect(reverse("collection:retrieve", args=[form.instance.id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
# @login_required
def retrieve(request, id):
if request.method == 'GET':
collection = get_object_or_404(Collection, pk=id)
if not collection.is_visible_to(request.user):
raise PermissionDenied()
form = CollectionForm(instance=collection)
following = True if request.user.is_authenticated and CollectionMark.objects.filter(owner=request.user, collection=collection).first() is not None else False
follower_count = CollectionMark.objects.filter(collection=collection).count()
return render(
request,
'detail.html',
{
'collection': collection,
'form': form,
'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
'follower_count': follower_count,
'following': following,
}
)
else:
logger.warning('non-GET method at /collections/<id>')
return HttpResponseBadRequest()
@mastodon_request_included
# @login_required
def retrieve_entity_list(request, id):
collection = get_object_or_404(Collection, pk=id)
if not collection.is_visible_to(request.user):
raise PermissionDenied()
form = CollectionForm(instance=collection)
return render(
request,
'entity_list.html',
{
'collection': collection,
'form': form,
'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
}
)
@login_required
def delete(request, id):
collection = get_object_or_404(Collection, pk=id)
if request.user.is_staff or request.user == collection.owner:
if request.method == 'GET':
return render(
request,
'delete.html',
{
'collection': collection,
'form': CollectionForm(instance=collection)
}
)
elif request.method == 'POST':
collection.delete()
return redirect(reverse("common:home"))
else:
raise PermissionDenied()
@login_required
def wish(request, id):
try:
CollectionMark.objects.create(owner=request.user, collection=Collection.objects.get(id=id))
except Exception:
pass
return HttpResponse("✔️")
@login_required
def follow(request, id):
CollectionMark.objects.create(owner=request.user, collection=Collection.objects.get(id=id))
return redirect(reverse("collection:retrieve", args=[id]))
@login_required
def unfollow(request, id):
CollectionMark.objects.filter(owner=request.user, collection=Collection.objects.get(id=id)).delete()
return redirect(reverse("collection:retrieve", args=[id]))
@login_required
def list(request, user_id=None, marked=False):
if request.method == 'GET':
user = request.user if user_id is None else User.objects.get(id=user_id)
if marked:
title = user.mastodon_username + _('关注的收藏单')
queryset = Collection.objects.filter(pk__in=CollectionMark.objects.filter(owner=user).values_list('collection', flat=True))
else:
title = user.mastodon_username + _('创建的收藏单')
queryset = Collection.objects.filter(owner=user)
paginator = Paginator(queryset, REVIEW_PER_PAGE)
page_number = request.GET.get('page', default=1)
collections = paginator.get_page(page_number)
collections.pagination = PageLinksGenerator(
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
return render(
request,
'list.html',
{
'collections': collections,
'title': title,
}
)
else:
return HttpResponseBadRequest()
def get_entity_by_url(url):
m = re.findall(r'^/?(movies|books|games|music/album|music/song)/(\d+)/?', url.strip().lower().replace(settings.APP_WEBSITE.lower(), ''))
if len(m) > 0:
mapping = {
'movies': Movie,
'books': Book,
'games': Game,
'music/album': Album,
'music/song': Song,
}
cls = mapping.get(m[0][0])
id = int(m[0][1])
if cls is not None:
return cls.objects.get(id=id)
return None
@login_required
def append_item(request, id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
url = request.POST.get('url')
comment = request.POST.get('comment')
item = get_entity_by_url(url)
collection.append_item(item, comment)
collection.save()
# return redirect(reverse("collection:retrieve", args=[id]))
return retrieve_entity_list(request, id)
else:
return HttpResponseBadRequest()
@login_required
def delete_item(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
item.delete()
# collection.save()
# return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id]))
return retrieve_entity_list(request, id)
return HttpResponseBadRequest()
@login_required
def move_up_item(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
items = collection.collectionitem_list
idx = items.index(item)
if idx > 0:
o = items[idx - 1]
p = o.position
o.position = item.position
item.position = p
o.save()
item.save()
# collection.save()
# return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id]))
return retrieve_entity_list(request, id)
return HttpResponseBadRequest()
@login_required
def move_down_item(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
items = collection.collectionitem_list
idx = items.index(item)
if idx + 1 < len(items):
o = items[idx + 1]
p = o.position
o.position = item.position
item.position = p
o.save()
item.save()
# collection.save()
# return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id]))
return retrieve_entity_list(request, id)
return HttpResponseBadRequest()
def show_item_comment(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
item = CollectionItem.objects.get(id=item_id)
editable = request.user.is_authenticated and collection.is_editable_by(request.user)
return render(request, 'show_item_comment.html', {'collection': collection, 'collectionitem': item, 'editable': editable})
@login_required
def update_item_comment(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
if request.method == 'POST':
item.comment = request.POST.get('comment', default='')
item.save()
return render(request, 'show_item_comment.html', {'collection': collection, 'collectionitem': item, 'editable': True})
else:
return render(request, 'edit_item_comment.html', {'collection': collection, 'collectionitem': item})
return retrieve_entity_list(request, id)
return HttpResponseBadRequest()
@login_required
def list_with(request, type, id):
pass
def get_entity_by_type_id(type, id):
mapping = {
'movie': Movie,
'book': Book,
'game': Game,
'album': Album,
'song': Song,
}
cls = mapping.get(type)
if cls is not None:
return cls.objects.get(id=id)
return None
@login_required
def add_to_list(request, type, id):
item = get_entity_by_type_id(type, id)
if request.method == 'GET':
queryset = Collection.objects.filter(owner=request.user)
return render(
request,
'add_to_list.html',
{
'type': type,
'id': id,
'item': item,
'collections': queryset,
}
)
else:
cid = int(request.POST.get('collection_id', default=0))
if not cid:
cid = Collection.objects.create(owner=request.user, title=f'{request.user.username}的收藏单').id
collection = Collection.objects.filter(owner=request.user, id=cid).first()
collection.append_item(item, request.POST.get('comment'))
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
@login_required
def share(request, id):
collection = Collection.objects.filter(id=id).first()
if not collection:
return HttpResponseBadRequest()
if request.method == 'GET':
return render(request, 'share_collection.html', {'id': id, 'visibility': request.user.get_preference().default_visibility})
else:
visibility = int(request.POST.get('visibility', default=0))
comment = request.POST.get('comment')
if share_collection(collection, comment, request.user, visibility):
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
else:
return go_relogin(request)

View file

View file

@ -1,8 +0,0 @@
from django.contrib import admin
from .models import *
from simple_history.admin import SimpleHistoryAdmin
admin.site.register(Game, SimpleHistoryAdmin)
admin.site.register(GameMark)
admin.site.register(GameReview)
admin.site.register(GameTag)

View file

@ -1,10 +0,0 @@
from django.apps import AppConfig
class GamesConfig(AppConfig):
name = 'games'
def ready(self):
from common.index import Indexer
from .models import Game
Indexer.update_model_indexable(Game)

View file

@ -1,85 +0,0 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _
from .models import Game, GameMark, GameReview, GameMarkStatusTranslation
from common.models import MarkStatusEnum
from common.forms import *
def GameMarkStatusTranslator(status):
return GameMarkStatusTranslation[status]
class GameForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
class Meta:
model = Game
fields = [
'id',
'title',
'source_site',
'source_url',
'other_title',
'developer',
'publisher',
'release_date',
'genre',
'platform',
'cover',
'brief',
'other_info'
]
widgets = {
'other_title': forms.TextInput(attrs={'placeholder': _("多个别名使用英文逗号分隔")}),
'developer': forms.TextInput(attrs={'placeholder': _("多个开发商使用英文逗号分隔")}),
'publisher': forms.TextInput(attrs={'placeholder': _("多个发行商使用英文逗号分隔")}),
'genre': forms.TextInput(attrs={'placeholder': _("多个类型使用英文逗号分隔")}),
'platform': forms.TextInput(attrs={'placeholder': _("多个平台使用英文逗号分隔")}),
'cover': PreviewImageInput(),
}
class GameMarkForm(MarkForm):
STATUS_CHOICES = [(v, GameMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = GameMark
fields = [
'id',
'game',
'status',
'rating',
'text',
'visibility',
]
widgets = {
'game': forms.TextInput(attrs={"hidden": ""}),
}
class GameReviewForm(ReviewForm):
class Meta:
model = GameReview
fields = [
'id',
'game',
'title',
'content',
'visibility'
]
widgets = {
'game': forms.TextInput(attrs={"hidden": ""}),
}

View file

@ -1,173 +0,0 @@
import uuid
import django.contrib.postgres.fields as postgres
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import reverse
from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
from django.utils import timezone
from django.conf import settings
from simple_history.models import HistoricalRecords
GameMarkStatusTranslation = {
MarkStatusEnum.DO.value: _("在玩"),
MarkStatusEnum.WISH.value: _("想玩"),
MarkStatusEnum.COLLECT.value: _("玩过")
}
def game_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.GAME_MEDIA_PATH_ROOT)
class Game(Entity):
"""
"""
title = models.CharField(_("名称"), max_length=500)
other_title = postgres.ArrayField(
models.CharField(blank=True,default='', max_length=500),
null=True,
blank=True,
default=list,
verbose_name=_("别名")
)
developer = postgres.ArrayField(
models.CharField(blank=True, default='', max_length=500),
null=True,
blank=True,
default=list,
verbose_name=_("开发商")
)
publisher = postgres.ArrayField(
models.CharField(blank=True, default='', max_length=500),
null=True,
blank=True,
default=list,
verbose_name=_("发行商")
)
release_date = models.DateField(
_('发行日期'),
auto_now=False,
auto_now_add=False,
null=True,
blank=True
)
genre = postgres.ArrayField(
models.CharField(blank=True, default='', max_length=200),
null=True,
blank=True,
default=list,
verbose_name=_("类型")
)
platform = postgres.ArrayField(
models.CharField(blank=True, default='', max_length=200),
null=True,
blank=True,
default=list,
verbose_name=_("平台")
)
cover = models.ImageField(_("封面"), upload_to=game_cover_path, default=settings.DEFAULT_GAME_IMAGE, blank=True)
history = HistoricalRecords()
def __str__(self):
return self.title
def get_json(self):
r = {
'developer': self.developer,
'other_title': self.other_title,
'publisher': self.publisher,
'release_date': self.release_date,
'platform': self.platform,
'genre': self.genre,
}
r.update(super().get_json())
return r
def get_absolute_url(self):
return reverse("games:retrieve", args=[self.id])
@property
def year(self):
return self.release_date.year if self.release_date else None
@property
def wish_url(self):
return reverse("games:wish", args=[self.id])
def get_tags_manager(self):
return self.game_tags
@property
def verbose_category_name(self):
return _("游戏")
@property
def mark_class(self):
return GameMark
@property
def tag_class(self):
return GameTag
class GameMark(Mark):
game = models.ForeignKey(
Game, on_delete=models.CASCADE, related_name='game_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'game'], name='unique_game_mark')
]
@property
def translated_status(self):
return GameMarkStatusTranslation[self.status]
class GameReview(Review):
game = models.ForeignKey(
Game, on_delete=models.CASCADE, related_name='game_reviews', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'game'], name='unique_game_review')
]
@property
def url(self):
return reverse("games:retrieve_review", args=[self.id])
@property
def item(self):
return self.game
class GameTag(Tag):
game = models.ForeignKey(
Game, on_delete=models.CASCADE, related_name='game_tags', null=True)
mark = models.ForeignKey(
GameMark, on_delete=models.CASCADE, related_name='gamemark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_gamemark_tag")
]
@property
def item(self):
return self.game

View file

@ -1,104 +0,0 @@
{% 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>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
{% if is_update and form.source_site.value != 'in-site' %}
<div style="float:right;padding-left:16px">
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
<div class="action-panel__button-group">
<form method="post" action="{% url 'games:rescrape' form.id.value %}">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
</form>
</div>
</div>
</div>
</div>
{% endif %}
<div class="single-section-wrapper" id="main">
{% comment %} <a href="{% url 'games:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
value="{{ form.instance.release_date | date:"Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -1,124 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/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.min.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 'games:retrieve' game.id %}">
<img src="{{ game.cover|thumb:'normal' }}" 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 'games:retrieve' game.id %}">
{{ game.title }}
</a>
<a href="{{ game.source_url }}"><span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span></a>
</h5>
<div>
{% if game.genre %}{% trans '类型:' %}
{% for genre in game.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if game.developer %}{% trans '开发商:' %}
{% for developer in game.developer %}
<span>{{ developer }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>{% if game.release_date %}
{% trans '发行日期:' %}{{ game.release_date }}
{% endif %}
</div>
{% if game.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ game.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.game }}
<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.visibility.label }}{{ form.visibility }}
</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>
<script>
</script>
</body>
</html>

View file

@ -1,99 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这个游戏吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'games:retrieve' game.id %}">
<img src="{{ game.cover|thumb:'normal' }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'games:retrieve' game.id %}">
<h5 class="entity-card__title">
{{ game.title }}
<a href="{{ game.source_url }}"><span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span></a>
</h5>
</a>
{% if game.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ game.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if game.last_editor %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' game.last_editor.mastodon_username %}">
<span>{{ game.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ game.edited_time }}</div>
{% if game.game_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ game.game_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if game.game_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ game.game_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'games:delete' game.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>
<script>
</script>
</body>
</html>

View file

@ -1,101 +0,0 @@
{% 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>{{ site_name }} - {% trans '删除评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'games: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>
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -1,421 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}游戏 - {{ game.title }}">
<meta property="og:type" content="game">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ game.cover.url }}">
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:description" content="{{ game.brief }}">
<title>{{ site_name }} - {% trans '游戏详情' %} | {{ game.title }}</title>
{% include "partial/_common_libs.html" with jquery=1 %}
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/detail.js' %}"></script>
<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">
<a href="{{ game.cover.url }}" class="entity-detail__img-origin" target="_blank" title="{% trans '查看原图' %}">
<img src="{{ game.cover|thumb:'normal' }}" class="entity-detail__img" alt="{{ game.title }}">
</a>
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ game.title }}
<a href="{{ game.source_url }}"><span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span></a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if game.rating and game.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ game.rating }} </span>
<small>({{ game.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if game.other_title %}{% trans '别名:' %}
{% for other_title in game.other_title %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="other_title">{{ other_title }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if game.other_title|length > 5 %}
<a href="javascript:void(0);" id="otherTitleMore">{% trans '更多' %}</a>
<script>
$("#otherTitleMore").on('click', function (e) {
$("span.other_title:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>
{% if game.genre %}{% trans '类型:' %}
{% for genre in game.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if game.developer %}{% trans '开发商:' %}
{% for developer in game.developer %}
<span>{{ developer }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if game.publisher %}{% trans '发行商:' %}
{% for publisher in game.publisher %}
<span>{{ publisher }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>{% if game.release_date %}
{% trans '发行日期:' %}{{ game.release_date }}
{% endif %}
</div>
{% if game.other_info %}
{% for k, v in game.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
</div>
<div class="entity-detail__fields">
<div>
{% if game.platform %}{% trans '平台:' %}
{% for platform in game.platform %}
<span>{{ platform }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
{% if game.last_editor and game.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.mastodon_username %}">{{ game.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'games:update' game.id %}">{% trans '编辑这个游戏' %}</a>
{% if user.is_staff %}
/<a href="{% url 'games:delete' game.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in game_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>
{% if game.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<p class="entity-desc__content">{{ game.brief | 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 'games:retrieve_mark_list' game.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
{% endif %}
<a href="{% url 'games:retrieve_mark_list' game.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
{% include "partial/mark_list.html" with mark_list=mark_list current_item=game %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这个游戏的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'games:retrieve_review_list' game.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.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'games:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
<span>{{ others_review.get_plain_content | truncate:100 }}</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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'games: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.created_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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">
<a href="{% url 'games:update_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'games:delete_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'games: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 'games:create_review' game.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
{% if collection_list %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关收藏单' %}</div>
<div >
{% for c in collection_list %}
<p>
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
</p>
{% endfor %}
<div class="action-panel__button-group action-panel__button-group--center">
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'game' game.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
</div>
</div>
</div>
</div>
{% endif %}
</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 'games:create_update_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.game }}
{% 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.visibility.label }}:</span>
{{ mark_form.visibility }}
</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

@ -1,129 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ game.title }}{% trans '的标记' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'games:retrieve' game.id %}">{{ game.title }}</a>{% trans ' 的标记' %}
</h5>
{% include "partial/mark_list.html" with mark_list=marks current_item=game %}
</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 'games:retrieve' game.id %}"><img src="{{ game.cover|thumb:'normal' }}" alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'games:retrieve' game.id %}">
{{ game.title }}
</a>
<a href="{{ game.source_url }}"><span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span></a>
</h5>
<div>
{% if game.genre %}{% trans '类型:' %}
{% for genre in game.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if game.developer %}{% trans '开发商:' %}
{% for developer in game.developer %}
<span>{{ developer }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>{% if game.release_date %}
{% trans '发行日期:' %}{{ game.release_date }}
{% endif %}
</div>
{% if game.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ game.rating | floatformat:'0'}}"> </span>
<span class="entity-card__rating-score rating-score"> {{ game.rating }} </span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,146 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}游戏评论 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ game.cover|thumb:'normal' }}">
<title>{{ site_name }}游戏评论 - {{ review.title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link" href="{% url 'games:update_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link" href="{% url 'games:delete_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
{% csrf_token %}
</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 'games:retrieve' game.id %}"><img src="{{ game.cover|thumb:'normal' }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title">
<a href="{% url 'games:retrieve' game.id %}">
{{ game.title }}
</a>
<a href="{{ game.source_url }}">
<span class="source-label source-label__{{ game.source_site }}">
{{ game.get_source_site_display }}
</span>
</a>
</h5>
<div>
{% if game.genre %}{% trans '类型:' %}
{% for genre in game.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if game.developer %}{% trans '开发商:' %}
{% for developer in game.developer %}
<span>{{ developer }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>{% if game.release_date %}
{% trans '发行日期:' %}{{ game.release_date }}
{% endif %}
</div>
{% if game.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ game.rating | floatformat:'0'}}"> </span>
<span class="entity-card__rating-score rating-score"> {{ game.rating }} </span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -1,147 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ game.title }}{% trans '的评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'games:retrieve' game.id %}">{{ game.title }}</a>{% trans ' 的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
<span href="{% url 'games:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'games: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 'games:retrieve' game.id %}"><img src="{{ game.cover|thumb:'normal' }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'games:retrieve' game.id %}">
{{ game.title }}
</a>
<a href="{{ game.source_url }}"><span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span></a>
</h5>
<div>
{% if game.genre %}{% trans '类型:' %}
{% for genre in game.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if game.developer %}{% trans '开发商:' %}
{% for developer in game.developer %}
<span>{{ developer }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>{% if game.release_date %}
{% trans '发行日期:' %}{{ game.release_date }}
{% endif %}
</div>
{% if game.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ game.rating | floatformat:'0'}}"> </span>
<span class="entity-card__rating-score rating-score"> {{ game.rating }} </span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,112 +0,0 @@
{% 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>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.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" src="https://www.douban.com/game/explore/?genres=&platforms=&sort=rating&q={% if q %}{{ q }}{% endif %}" frameborder="0"></iframe>
<div class="dividing-line"></div>
<div id="scrapeForm">
<form action="{% url 'games:create' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}" value="{{ form.instance.release_date | date:"
Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
</form>
<a href="#" 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 'games:click_to_scrape' %}" method="post">
{% csrf_token %}
<input type="text" name="url" required placeholder="https://game.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>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,24 +0,0 @@
from django.urls import path, re_path
from .views import *
app_name = 'games'
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('rescrape/<int:id>/', rescrape, name='rescrape'),
path('mark/', create_update_mark, name='create_update_mark'),
path('wish/<int:id>/', wish, name='wish'),
re_path('(?P<game_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
path('<int:game_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:game_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'),
]

View file

@ -1,585 +0,0 @@
import logging
from django.shortcuts import render, get_object_or_404, redirect, reverse
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse
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 mastodon import mastodon_request_included
from mastodon.models import MastodonApplication
from mastodon.api import share_mark, share_review
from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
from common.models import SourceSiteEnum
from .models import *
from .forms import *
from django.conf import settings
from collection.models import CollectionItem
from common.scraper import get_scraper_by_url, get_normalized_url
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 = GameForm()
return render(
request,
'games/create_update.html',
{
'form': form,
'title': _('添加游戏'),
'submit_url': reverse("games:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
if request.user.is_authenticated:
# only local user can alter public data
form = GameForm(request.POST, request.FILES)
if form.is_valid():
form.instance.last_editor = request.user
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
return redirect(reverse("games:retrieve", args=[form.instance.id]))
else:
return render(
request,
'games/create_update.html',
{
'form': form,
'title': _('添加游戏'),
'submit_url': reverse("games:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
else:
return redirect(reverse("users:login"))
else:
return HttpResponseBadRequest()
@login_required
def rescrape(request, id):
if request.method != 'POST':
return HttpResponseBadRequest()
item = get_object_or_404(Game, pk=id)
url = get_normalized_url(item.source_url)
scraper = get_scraper_by_url(url)
scraper.scrape(url)
form = scraper.save(request_user=request.user, instance=item)
return redirect(reverse("games:retrieve", args=[form.instance.id]))
@login_required
def update(request, id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
form = GameForm(instance=game)
page_title = _('修改游戏')
return render(
request,
'games/create_update.html',
{
'form': form,
'is_update': True,
'title': page_title,
'submit_url': reverse("games:update", args=[game.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
game = get_object_or_404(Game, pk=id)
form = GameForm(request.POST, request.FILES, instance=game)
page_title = _("修改游戏")
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
else:
return render(
request,
'games/create_update.html',
{
'form': form,
'is_update': True,
'title': page_title,
'submit_url': reverse("games:update", args=[game.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
return redirect(reverse("games:retrieve", args=[form.instance.id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
# @login_required
def retrieve(request, id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
mark = None
mark_tags = None
review = None
# retreive tags
game_tag_list = game.game_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 = GameMark.objects.get(owner=request.user, game=game)
except ObjectDoesNotExist:
mark = None
if mark:
mark_tags = mark.gamemark_tags.all()
mark.get_status_display = GameMarkStatusTranslator(mark.status)
mark_form = GameMarkForm(instance=mark, initial={
'tags': mark_tags
})
else:
mark_form = GameMarkForm(initial={
'game': game,
'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
'tags': mark_tags
})
# retrieve user review
try:
if request.user.is_authenticated:
review = GameReview.objects.get(
owner=request.user, game=game)
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 = GameMark.get_available(game, request.user)
review_list = GameReview.get_available(game, request.user)
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 = GameMarkStatusTranslator(m.status)
review_list_more = True if len(
review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER]
all_collections = CollectionItem.objects.filter(game=game).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# 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,
'games/detail.html',
{
'game': game,
'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,
'game_tag_list': game_tag_list,
'mark_tags': mark_tags,
'collection_list': collection_list,
}
)
else:
logger.warning('non-GET method at /games/<id>')
return HttpResponseBadRequest()
@permission_required("games.delete_game")
@login_required
def delete(request, id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
return render(
request,
'games/delete.html',
{
'game': game,
}
)
elif request.method == 'POST':
if request.user.is_staff:
# only staff has right to delete
game = get_object_or_404(Game, pk=id)
game.delete()
return redirect(reverse("common:home"))
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 game rating
# owner check(guarantee)
if request.method == 'POST':
pk = request.POST.get('id')
old_rating = None
old_tags = None
if not pk:
game_id = request.POST.get('game')
mark = GameMark.objects.filter(game_id=game_id, owner=request.user).first()
if mark:
pk = mark.id
if pk:
mark = get_object_or_404(GameMark, pk=pk)
if request.user != mark.owner:
return HttpResponseBadRequest()
old_rating = mark.rating
old_tags = mark.gamemark_tags.all()
if mark.status != request.POST.get('status'):
mark.created_time = timezone.now()
# update
form = GameMarkForm(request.POST, instance=mark)
else:
# create
form = GameMarkForm(request.POST)
if form.is_valid():
if form.instance.status == MarkStatusEnum.WISH.value or form.instance.rating == 0:
form.instance.rating = None
form.cleaned_data['rating'] = None
form.instance.owner = request.user
form.instance.edited_time = timezone.now()
game = form.instance.game
try:
with transaction.atomic():
# update game rating
game.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']:
GameTag.objects.create(
content=tag,
game=game,
mark=form.instance
)
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
if form.cleaned_data['share_to_mastodon']:
if not share_mark(form.instance):
return go_relogin(request)
else:
return HttpResponseBadRequest(f"invalid form data {form.errors}")
return redirect(reverse("games:retrieve", args=[form.instance.game.id]))
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def wish(request, id):
if request.method == 'POST':
game = get_object_or_404(Game, pk=id)
params = {
'owner': request.user,
'status': MarkStatusEnum.WISH,
'visibility': request.user.preference.default_visibility,
'game': game,
}
try:
GameMark.objects.create(**params)
except Exception:
pass
return HttpResponse("✔️")
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def retrieve_mark_list(request, game_id, following_only=False):
if request.method == 'GET':
game = get_object_or_404(Game, pk=game_id)
queryset = GameMark.get_available(game, request.user, following_only=following_only)
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 = GameMarkStatusTranslator(m.status)
return render(
request,
'games/mark_list.html',
{
'marks': marks,
'game': game,
}
)
else:
return HttpResponseBadRequest()
@login_required
def delete_mark(request, id):
if request.method == 'POST':
mark = get_object_or_404(GameMark, pk=id)
if request.user != mark.owner:
return HttpResponseBadRequest()
game_id = mark.game.id
try:
with transaction.atomic():
# update game rating
mark.game.update_rating(mark.rating, None)
mark.delete()
except IntegrityError as e:
return HttpResponseServerError()
return redirect(reverse("games:retrieve", args=[game_id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def create_review(request, game_id):
if request.method == 'GET':
form = GameReviewForm(initial={'game': game_id})
game = get_object_or_404(Game, pk=game_id)
return render(
request,
'games/create_update_review.html',
{
'form': form,
'title': _("添加评论"),
'game': game,
'submit_url': reverse("games:create_review", args=[game_id]),
}
)
elif request.method == 'POST':
form = GameReviewForm(request.POST)
if form.is_valid():
form.instance.owner = request.user
form.save()
if form.cleaned_data['share_to_mastodon']:
if not share_review(form.instance):
return go_relogin(request)
return redirect(reverse("games:retrieve_review", args=[form.instance.id]))
else:
return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def update_review(request, id):
if request.method == 'GET':
review = get_object_or_404(GameReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
form = GameReviewForm(instance=review)
game = review.game
return render(
request,
'games/create_update_review.html',
{
'form': form,
'title': _("编辑评论"),
'game': game,
'submit_url': reverse("games:update_review", args=[review.id]),
}
)
elif request.method == 'POST':
review = get_object_or_404(GameReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
form = GameReviewForm(request.POST, instance=review)
if form.is_valid():
form.instance.edited_time = timezone.now()
form.save()
if form.cleaned_data['share_to_mastodon']:
if not share_review(form.instance):
return go_relogin(request)
return redirect(reverse("games: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(GameReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
review_form = GameReviewForm(instance=review)
return render(
request,
'games/delete_review.html',
{
'form': review_form,
'review': review,
}
)
elif request.method == 'POST':
review = get_object_or_404(GameReview, pk=id)
if request.user != review.owner:
return HttpResponseBadRequest()
game_id = review.game.id
review.delete()
return redirect(reverse("games:retrieve", args=[game_id]))
else:
return HttpResponseBadRequest()
@mastodon_request_included
def retrieve_review(request, id):
if request.method == 'GET':
review = get_object_or_404(GameReview, pk=id)
if not review.is_visible_to(request.user):
msg = _("你没有访问这个页面的权限😥")
return render(
request,
'common/error.html',
{
'msg': msg,
}
)
review_form = GameReviewForm(instance=review)
game = review.game
try:
mark = GameMark.objects.get(owner=review.owner, game=game)
mark.get_status_display = GameMarkStatusTranslator(mark.status)
except ObjectDoesNotExist:
mark = None
return render(
request,
'games/review_detail.html',
{
'form': review_form,
'review': review,
'game': game,
'mark': mark,
}
)
else:
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def retrieve_review_list(request, game_id):
if request.method == 'GET':
game = get_object_or_404(Game, pk=game_id)
queryset = GameReview.get_available(game, request.user)
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,
'games/review_list.html',
{
'reviews': reviews,
'game': game,
}
)
else:
return HttpResponseBadRequest()
@login_required
def scrape(request):
if request.method == 'GET':
keywords = request.GET.get('q')
form = GameForm()
return render(
request,
'games/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:
return jump_or_scrape(request, url)
else:
return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()

View file

View file

@ -1,8 +0,0 @@
from django.contrib import admin
from .models import *
from simple_history.admin import SimpleHistoryAdmin
admin.site.register(Movie, SimpleHistoryAdmin)
admin.site.register(MovieMark)
admin.site.register(MovieReview)
admin.site.register(MovieTag)

View file

@ -1,10 +0,0 @@
from django.apps import AppConfig
class MoviesConfig(AppConfig):
name = 'movies'
def ready(self):
from common.index import Indexer
from .models import Movie
Indexer.update_model_indexable(Movie)

View file

@ -1,137 +0,0 @@
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, MovieGenreEnum, MovieMarkStatusTranslation
from common.models import MarkStatusEnum
from common.forms import *
def MovieMarkStatusTranslator(status):
return MovieMarkStatusTranslation[status]
class MovieForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
genre = forms.MultipleChoiceField(
required=False,
choices=MovieGenreEnum.choices,
widget= MultiSelect,
label=_("类型")
)
showtime = HstoreField(
required=False,
label=_("上映时间"),
widget=HstoreInput(
attrs={
'placeholder-key': _("日期"),
'placeholder-value': _("地区"),
}
)
)
other_info = JSONField(required=False, label=_("其他信息"))
class Meta:
model = Movie
fields = [
'id',
'title',
'source_site',
'source_url',
'orig_title',
'other_title',
'imdb_code',
'director',
'playwright',
'actor',
'genre',
'showtime',
'site',
'area',
'language',
'year',
'duration',
'season',
'episodes',
'single_episode_length',
'cover',
'is_series',
'brief',
'other_info',
]
labels = {
'title': _("标题"),
'orig_title': _("原名"),
'other_title': _("又名"),
'imdb_code': _("IMDb编号"),
'director': _("导演"),
'playwright': _("编剧"),
'actor': _("主演"),
'genre': _("类型"),
'showtime': _("上映时间"),
'site': _("官方网站"),
'area': _("国家/地区"),
'language': _("语言"),
'year': _("年份"),
'duration': _("片长"),
'season': _("季数"),
'episodes': _("集数"),
'single_episode_length': _("单集片长"),
'cover': _("封面"),
'brief': _("简介"),
'other_info': _("其他信息"),
'is_series': _("是否为剧集"),
}
widgets = {
'other_title': forms.TextInput(attrs={'placeholder': _("多个别名使用英文逗号分隔")}),
'director': forms.TextInput(attrs={'placeholder': _("多个导演使用英文逗号分隔")}),
'actor': forms.TextInput(attrs={'placeholder': _("多个主演使用英文逗号分隔")}),
'playwright': forms.TextInput(attrs={'placeholder': _("多个编剧使用英文逗号分隔")}),
'area': forms.TextInput(attrs={'placeholder': _("多个国家/地区使用英文逗号分隔")}),
'language': forms.TextInput(attrs={'placeholder': _("多种语言使用英文逗号分隔")}),
'cover': PreviewImageInput(),
'is_series': forms.CheckboxInput(attrs={'style': 'width: auto; position: relative; top: 2px'})
}
class MovieMarkForm(MarkForm):
STATUS_CHOICES = [(v, MovieMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = MovieMark
fields = [
'id',
'movie',
'status',
'rating',
'text',
'visibility',
]
widgets = {
'movie': forms.TextInput(attrs={"hidden": ""}),
}
class MovieReviewForm(ReviewForm):
class Meta:
model = MovieReview
fields = [
'id',
'movie',
'title',
'content',
'visibility'
]
widgets = {
'movie': forms.TextInput(attrs={"hidden": ""}),
}

View file

@ -1,203 +0,0 @@
from django.core.management.base import BaseCommand
from django.core.files.uploadedfile import SimpleUploadedFile
from common.scraper import *
from django.conf import settings
from movies.models import Movie
from movies.forms import MovieForm
import requests
import re
import filetype
from lxml import html
from PIL import Image
from io import BytesIO
class DoubanPatcherMixin:
@classmethod
def download_page(cls, url, headers):
url = cls.get_effective_url(url)
r = None
error = 'DoubanScrapper: error occured when downloading ' + url
content = None
def get(url, timeout):
nonlocal r
# print('Douban GET ' + url)
try:
r = requests.get(url, timeout=timeout)
except Exception as e:
r = requests.Response()
r.status_code = f"Exception when GET {url} {e}" + url
# print('Douban CODE ' + str(r.status_code))
return r
def check_content():
nonlocal r, error, content
content = None
if r.status_code == 200:
content = r.content.decode('utf-8')
if content.find('关于豆瓣') == -1:
if content.find('你的 IP 发出') == -1:
error = error + 'Content not authentic' # response is garbage
else:
error = error + 'IP banned'
content = None
elif re.search('不存在[^<]+</title>', content, re.MULTILINE):
content = None
error = error + 'Not found or hidden by Douban'
else:
error = error + str(r.status_code)
def fix_wayback_links():
nonlocal content
# fix links
content = re.sub(r'href="http[^"]+http', r'href="http', content)
# https://img9.doubanio.com/view/subject/{l|m|s}/public/s1234.jpg
content = re.sub(r'src="[^"]+/(s\d+\.\w+)"',
r'src="https://img9.doubanio.com/view/subject/m/public/\1"', content)
# https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2681329386.jpg
# https://img9.doubanio.com/view/photo/{l|m|s}/public/p1234.webp
content = re.sub(r'src="[^"]+/(p\d+\.\w+)"',
r'src="https://img9.doubanio.com/view/photo/m/public/\1"', content)
# Wayback Machine: get latest available
def wayback():
nonlocal r, error, content
error = error + '\nWayback: '
get('http://archive.org/wayback/available?url=' + url, 10)
if r.status_code == 200:
w = r.json()
if w['archived_snapshots'] and w['archived_snapshots']['closest']:
get(w['archived_snapshots']['closest']['url'], 10)
check_content()
if content is not None:
fix_wayback_links()
else:
error = error + 'No snapshot available'
else:
error = error + str(r.status_code)
# Wayback Machine: guess via CDX API
def wayback_cdx():
nonlocal r, error, content
error = error + '\nWayback: '
get('http://web.archive.org/cdx/search/cdx?url=' + url, 10)
if r.status_code == 200:
dates = re.findall(r'[^\s]+\s+(\d+)\s+[^\s]+\s+[^\s]+\s+\d+\s+[^\s]+\s+\d{5,}',
r.content.decode('utf-8'))
# assume snapshots whose size >9999 contain real content, use the latest one of them
if len(dates) > 0:
get('http://web.archive.org/web/' + dates[-1] + '/' + url, 10)
check_content()
if content is not None:
fix_wayback_links()
else:
error = error + 'No snapshot available'
else:
error = error + str(r.status_code)
def latest():
nonlocal r, error, content
if settings.SCRAPESTACK_KEY is None:
error = error + '\nDirect: '
get(url, 60)
else:
error = error + '\nScraperAPI: '
get(f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}', 60)
check_content()
# wayback_cdx()
# if content is None:
latest()
if content is None:
logger.error(error)
content = '<html />'
# with open('/tmp/temp.html', 'w', encoding='utf-8') as fp:
# fp.write(content)
return html.fromstring(content)
@classmethod
def download_image(cls, url, item_url=None):
if url is None:
return None, None
raw_img = None
ext = None
dl_url = url
if settings.SCRAPESTACK_KEY is not None:
dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
try:
img_response = requests.get(dl_url, timeout=90)
if img_response.status_code == 200:
raw_img = img_response.content
img = Image.open(BytesIO(raw_img))
img.load() # corrupted image will trigger exception
content_type = img_response.headers.get('Content-Type')
ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension
else:
logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}")
# raise RuntimeError(f"Douban: download image failed {img_response.status_code} {dl_url}")
except Exception as e:
raw_img = None
ext = None
logger.error(f"Douban: download image failed {e} {dl_url} {item_url}")
if raw_img is None and settings.SCRAPESTACK_KEY is not None:
try:
img_response = requests.get(dl_url, timeout=90)
if img_response.status_code == 200:
raw_img = img_response.content
img = Image.open(BytesIO(raw_img))
img.load() # corrupted image will trigger exception
content_type = img_response.headers.get('Content-Type')
ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension
else:
logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}")
except Exception as e:
raw_img = None
ext = None
logger.error(f"Douban: download image failed {e} {dl_url} {item_url}")
return raw_img, ext
class DoubanMoviePatcher(DoubanPatcherMixin, AbstractScraper):
site_name = SourceSiteEnum.DOUBAN.value
host = 'movie.douban.com'
data_class = Movie
form_class = MovieForm
regex = re.compile(r"https://movie\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
headers['Host'] = self.host
content = self.download_page(url, headers)
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, ext = self.download_image(img_url, url)
return raw_img, ext
class Command(BaseCommand):
help = 'fix cover image'
def add_arguments(self, parser):
parser.add_argument('threadId', type=int, help='% 8')
def handle(self, *args, **options):
t = int(options['threadId'])
for m in Movie.objects.filter(cover='movie/default.svg', source_site='douban'):
if m.id % 8 == t:
print(f'Re-fetching {m.source_url}')
try:
raw_img, img_ext = DoubanMoviePatcher.scrape(m.source_url)
if img_ext is not None:
m.cover = SimpleUploadedFile('temp.' + img_ext, raw_img)
m.save()
print(f'Saved {m.source_url}')
else:
print(f'Skipped {m.source_url}')
except Exception as e:
print(e)
# return

View file

@ -1,293 +0,0 @@
import uuid
import django.contrib.postgres.fields as postgres
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import reverse
from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
from django.utils import timezone
from django.conf import settings
from django.db.models import Q
import re
from simple_history.models import HistoricalRecords
MovieMarkStatusTranslation = {
MarkStatusEnum.DO.value: _("在看"),
MarkStatusEnum.WISH.value: _("想看"),
MarkStatusEnum.COLLECT.value: _("看过")
}
def movie_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.MOVIE_MEDIA_PATH_ROOT)
class MovieGenreEnum(models.TextChoices):
DRAMA = 'Drama', _('剧情')
KIDS = 'Kids', _('儿童')
COMEDY = 'Comedy', _('喜剧')
BIOGRAPHY = 'Biography', _('传记')
ACTION = 'Action', _('动作')
HISTORY = 'History', _('历史')
ROMANCE = 'Romance', _('爱情')
WAR = 'War', _('战争')
SCI_FI = 'Sci-Fi', _('科幻')
CRIME = 'Crime', _('犯罪')
ANIMATION = 'Animation', _('动画')
WESTERN = 'Western', _('西部')
MYSTERY = 'Mystery', _('悬疑')
FANTASY = 'Fantasy', _('奇幻')
THRILLER = 'Thriller', _('惊悚')
ADVENTURE = 'Adventure', _('冒险')
HORROR = 'Horror', _('恐怖')
DISASTER = 'Disaster', _('灾难')
DOCUMENTARY = 'Documentary', _('纪录片')
MARTIAL_ARTS = 'Martial-Arts', _('武侠')
SHORT = 'Short', _('短片')
ANCIENT_COSTUM = 'Ancient-Costum', _('古装')
EROTICA = 'Erotica', _('情色')
SPORT = 'Sport', _('运动')
GAY_LESBIAN = 'Gay/Lesbian', _('同性')
OPERA = 'Opera', _('戏曲')
MUSIC = 'Music', _('音乐')
FILM_NOIR = 'Film-Noir', _('黑色电影')
MUSICAL = 'Musical', _('歌舞')
REALITY_TV = 'Reality-TV', _('真人秀')
FAMILY = 'Family', _('家庭')
TALK_SHOW = 'Talk-Show', _('脱口秀')
NEWS = 'News', _('新闻')
SOAP = 'Soap', _('肥皂剧')
TV_MOVIE = 'TV Movie', _('电视电影')
THEATRE = 'Theatre', _('舞台艺术')
OTHER = 'Other', _('其他')
MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum)
class Movie(Entity):
'''
Can either be movie or series.
'''
# widely recognized name, usually in Chinese
title = models.CharField(_("title"), max_length=500)
# original name, for books in foreign language
orig_title = models.CharField(
_("original title"), blank=True, default='', max_length=500)
other_title = postgres.ArrayField(
models.CharField(_("other title"), blank=True,
default='', max_length=500),
null=True,
blank=True,
default=list,
)
imdb_code = models.CharField(
blank=True, max_length=10, null=False, db_index=True, default='')
director = postgres.ArrayField(
models.CharField(_("director"), blank=True,
default='', max_length=200),
null=True,
blank=True,
default=list,
)
playwright = postgres.ArrayField(
models.CharField(_("playwright"), blank=True,
default='', max_length=200),
null=True,
blank=True,
default=list,
)
actor = postgres.ArrayField(
models.CharField(_("actor"), blank=True,
default='', max_length=200),
null=True,
blank=True,
default=list,
)
genre = postgres.ArrayField(
models.CharField(
_("genre"),
blank=True,
default='',
choices=MovieGenreEnum.choices,
max_length=50
),
null=True,
blank=True,
default=list,
)
showtime = postgres.ArrayField(
# HStoreField stores showtime-region pair
postgres.HStoreField(),
null=True,
blank=True,
default=list,
)
site = models.URLField(_('site url'), blank=True, default='', max_length=200)
# country or region
area = postgres.ArrayField(
models.CharField(
_("country or region"),
blank=True,
default='',
max_length=100,
),
null=True,
blank=True,
default=list,
)
language = postgres.ArrayField(
models.CharField(
blank=True,
default='',
max_length=100,
),
null=True,
blank=True,
default=list,
)
year = models.PositiveIntegerField(null=True, blank=True)
duration = models.CharField(blank=True, default='', max_length=200)
cover = models.ImageField(_("poster"), upload_to=movie_cover_path, default=settings.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)
# deprecated
# tv_station = models.CharField(blank=True, default='', max_length=200)
single_episode_length = models.CharField(blank=True, default='', max_length=100)
############################################
# category identifier
############################################
is_series = models.BooleanField(default=False)
history = HistoricalRecords()
def __str__(self):
if self.year:
return self.title + f"({self.year})"
else:
return self.title
def get_json(self):
r = {
'other_title': self.other_title,
'original_title': self.orig_title,
'director': self.director,
'playwright': self.playwright,
'actor': self.actor,
'release_year': self.year,
'genre': self.genre,
'language': self.language,
'season': self.season,
'duration': self.duration,
'imdb_code': self.imdb_code,
}
r.update(super().get_json())
return r
def get_absolute_url(self):
return reverse("movies:retrieve", args=[self.id])
@property
def wish_url(self):
return reverse("movies:wish", args=[self.id])
def get_tags_manager(self):
return self.movie_tags
def get_genre_display(self):
translated_genre = []
for g in self.genre:
translated_genre.append(MovieGenreTranslator[g])
return translated_genre
def get_related_movies(self):
imdb = 'no match' if self.imdb_code is None or self.imdb_code == '' else self.imdb_code
qs = Q(imdb_code=imdb)
if self.is_series:
prefix = re.sub(r'\d+', '', re.sub(r'\s+第.+季', '', self.title))
if not prefix:
prefix = self.title
qs = qs | Q(title__startswith=prefix)
qs = qs & ~Q(id=self.id)
return Movie.objects.filter(qs).order_by('season')
def get_identicals(self):
qs = Q(orig_title=self.title)
if self.imdb_code:
qs = Q(imdb_code=self.imdb_code)
# qs = qs & ~Q(id=self.id)
return Movie.objects.filter(qs)
else:
return [self] # Book.objects.filter(id=self.id)
@property
def verbose_category_name(self):
if self.is_series:
return _("剧集")
else:
return _("电影")
@property
def mark_class(self):
return MovieMark
@property
def tag_class(self):
return MovieTag
class MovieMark(Mark):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['owner', 'movie'], name='unique_movie_mark')
]
@property
def translated_status(self):
return MovieMarkStatusTranslation[self.status]
class MovieReview(Review):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_reviews', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'movie'], name='unique_movie_review')
]
@property
def url(self):
return reverse("movies:retrieve_review", args=[self.id])
@property
def item(self):
return self.movie
class MovieTag(Tag):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_tags', null=True)
mark = models.ForeignKey(MovieMark, on_delete=models.CASCADE, related_name='moviemark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_moviemark_tag")
]
@property
def item(self):
return self.movie

View file

@ -1,102 +0,0 @@
{% 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>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid" class="single-section-wrapper">
{% if is_update and form.source_site.value != 'in-site' %}
<div style="float:right;padding-left:16px">
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
<div class="action-panel__button-group">
<form method="post" action="{% url 'movies:rescrape' form.id.value %}">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
</form>
</div>
</div>
</div>
</div>
{% endif %}
<div class="single-section-wrapper" id="main">
{% comment %} <a href="{% url 'movies:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.id_for_label == 'id_is_series' %}
<label for="{{ field.id_for_label }}"
style="display: inline-block; position: relative;">{{ field.label }}</label>
{{ field }}
{% else %}
{% if field.id_for_label != 'id_id' %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -1,168 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/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.min.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|thumb:'normal' }}" 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 %}">
{% if movie.season %}
{{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season
{{ movie.season }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% else %}
{{ movie.title }} {{ movie.orig_title }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% endif %}
</a>
<a href="{{ movie.source_url }}"><span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span></a>
</h5>
<div>{% if movie.director %}{% trans '导演:' %}
{% for director in movie.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.genre %}{% trans '类型:' %}
{% for genre in movie.get_genre_display %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.actor %}{% trans '主演:' %}
{% for actor in movie.actor %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="actor">{{ actor }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% if forloop.counter <= 5 %}
{% endif %}
{% endfor %}
{% if movie.actor|length > 5 %}
<a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
<script>
$("#actorMore").on('click', function (e) {
$("span.actor:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.showtime %}{% trans '上映时间:' %}
{% for showtime in movie.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}({{ region }})</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% 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.visibility.label }}{{ form.visibility }}
</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>
<script>
</script>
</body>
</html>

View file

@ -1,106 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="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|thumb:'normal' }}" 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">
{% if movie.season %}
{{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season
{{ movie.season }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% else %}
{{ movie.title }} {{ movie.orig_title }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% endif %}
<a href="{{ movie.source_url }}"><span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span></a>
</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 %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' movie.last_editor.mastodon_username %}">
<span>{{ movie.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ movie.edited_time }}</div>
{% if movie.movie_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ movie.movie_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if movie.movie_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ movie.movie_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</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>
<script>
</script>
</body>
</html>

View file

@ -1,101 +0,0 @@
{% 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>{{ site_name }} - {% trans '删除评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</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>
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -1,527 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}电影 - {{ movie.title }}">
<meta property="og:type" content="video.movie">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ movie.cover.url }}">
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:description" content="{{ movie.brief }}">
<!--
video:actor - profile array - Actors in the movie.
video:actor:role - string - The role they played.
video:director - profile array - Directors of the movie.
video:writer - profile array - Writers of the movie.
video:duration - integer >=1 - The movie's length in seconds.
video:release_date - datetime - The date the movie was released.
video:tag - string array - Tag words associated with this movie.
-->
{% if movie.is_series %}
<title>{{ site_name }} - {% trans '剧集详情' %} | {{ movie.title }}</title>
{% else %}
<title>{{ site_name }} - {% trans '电影详情' %} | {{ movie.title }}</title>
{% endif %}
{% include "partial/_common_libs.html" with jquery=1 %}
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/detail.js' %}"></script>
</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">
<a href="{{ movie.cover.url }}" class="entity-detail__img-origin" target="_blank" title="{% trans '查看原图' %}">
<img src="{{ movie.cover|thumb:'normal' }}" class="entity-detail__img" alt="{{ movie.title }}">
</a>
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{% if movie.season %}
{{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season {{ movie.season }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if movie.year %}({{ movie.year }}){% endif %}
</span>
{% else %}
{{ movie.title }} {{ movie.orig_title }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if movie.year %}({{ movie.year }}){% endif %}
</span>
{% endif %}
<a href="{{ movie.source_url }}"><span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span></a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if movie.rating and movie.rating_number >= 5 %}
<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>
<small>({{ movie.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if movie.imdb_code %}
{% trans 'IMDb' %}<a href="https://www.imdb.com/title/{{ movie.imdb_code }}/" target="_blank">{{ movie.imdb_code }}</a>
{% endif %}
</div>
<div>{% if movie.director %}{% trans '导演:' %}
{% for director in movie.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.playwright %}{% trans '编剧:' %}
{% for playwright in movie.playwright %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="playwright">{{ playwright }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.playwright|length > 5 %}
<a href="javascript:void(0);" id="playwrightMore">{% trans '更多' %}</a>
<script>
$("#playwrightMore").on('click', function (e) {
$("span.playwright:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.actor %}{% trans '主演:' %}
{% for actor in movie.actor %}
<span {% if forloop.counter > 5 %}style="display: none;"{% endif %}>
<span class="actor">{{ actor }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.actor|length > 5 %}
<a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
<script>
$("#actorMore").on('click', function(e) {
$("span.actor:not(:visible)").each(function(e){
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.genre %}{% trans '类型:' %}
{% for genre in movie.get_genre_display %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.area %}{% trans '制片国家/地区:' %}
{% for area in movie.area %}
<span>{{ area }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.language %}{% trans '语言:' %}
{% for language in movie.language %}
<span>{{ language }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
</div>
<div class="entity-detail__fields">
<div>{% if movie.duration %}{% trans '片长:' %}{{ movie.duration }}{% endif %}</div>
<div>{% if movie.season %}{% trans '季数:' %}{{ movie.season }}{% endif %}</div>
<div>{% if movie.episodes %}{% trans '集数:' %}{{ movie.episodes }}{% endif %}</div>
<div>{% if movie.single_episode_length %}{% trans '单集长度:' %}{{ movie.single_episode_length }}{% endif %}</div>
<div>{% if movie.showtime %}{% trans '上映时间:' %}
{% for showtime in movie.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}{% if region != '' %}({{ region }}){% endif %}</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.other_title %}{% trans '又名:' %}
{% for other_title in movie.other_title %}
<span>{{ other_title }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.site %}{% trans '网站:' %}
<a href="{{ movie.site }}" target="_blank">{{ movie.site|strip_scheme }}</a>
{% endif %}</div>
{% if movie.other_info %}
{% for k, v in movie.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if movie.last_editor and movie.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.mastodon_username %}">{{ movie.last_editor | default:"" }}</a></div>
{% endif %}
<div>
{% if movie.is_series %}
<a href="{% url 'movies:update' movie.id %}">{% trans '编辑这部剧集' %}</a>
{% else %}
<a href="{% url 'movies:update' movie.id %}">{% trans '编辑这部电影' %}</a>
{% endif %}
{% 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>
{% if movie.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<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>
</div>
{% endif %}
<div class="entity-marks">
{% if movie.is_series %}
<h5 class="entity-marks__title">{% trans '这部剧集的标记' %}</h5>
{% else %}
<h5 class="entity-marks__title">{% trans '这部电影的标记' %}</h5>
{% endif %}
{% if mark_list_more %}
<a href="{% url 'movies:retrieve_mark_list' movie.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
{% endif %}
<a href="{% url 'movies:retrieve_mark_list' movie.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
{% include "partial/mark_list.html" with mark_list=mark_list current_item=movie %}
</div>
<div class="entity-reviews">
{% if movie.is_series %}
<h5 class="entity-reviews__title">{% trans '这部剧集的评论' %}</h5>
{% else %}
<h5 class="entity-reviews__title">{% trans '这部电影的评论' %}</h5>
{% endif %}
{% 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.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
{% if others_review.movie != movie %}
<span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'movies:retrieve' others_review.movie.id %}">{{ others_review.movie.get_source_site_display }}</a></span>
{% endif %}
<span class="entity-reviews__review-title"> <a href="{% url 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
<span>{{ others_review.get_plain_content | truncate:100 }}</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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="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.created_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">
{% if movie.is_series %}
<div class="action-panel__label">{% trans '标记这部剧集' %}</div>
{% else %}
<div class="action-panel__label">{% trans '标记这部电影' %}</div>
{% endif %}
<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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="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>
{% if movie.get_related_movies.count > 0 %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关条目' %}</div>
<div >
{% for m in movie.get_related_movies %}
<p>
<a href="{% url 'movies:retrieve' m.id %}">{{ m.title }}</a>
{% if movie.source_site != m.source_site %}
<span class="source-label source-label__{{ m.source_site }}">{{ m.get_source_site_display }}</span>
{% endif %}
</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if collection_list %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关收藏单' %}</div>
<div >
{% for c in collection_list %}
<p>
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
</p>
{% endfor %}
<div class="action-panel__button-group action-panel__button-group--center">
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'movie' movie.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
</div>
</div>
</div>
</div>
{% endif %}
</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 %}
{% if movie.is_series %}
<style>
.mark-modal__title::after {
content: "{% trans '这部剧集' %}";
}
</style>
{% else %}
<style>
.mark-modal__title::after {
content: "{% trans '这部电影' %}";
}
</style>
{% endif %}
<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.visibility.label }}:</span>
{{ mark_form.visibility }}
</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

@ -1,148 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ movie.title }}{% trans '的标记' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a>{% trans ' 的标记' %}
</h5>
{% include "partial/mark_list.html" with mark_list=marks current_item=movie %}
</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|thumb:'normal' }}" 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 %}">
{% if movie.season %}
{{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season
{{ movie.season }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% else %}
{{ movie.title }} {{ movie.orig_title }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% endif %}
</a>
<a href="{{ movie.source_url }}"><span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span></a>
</h5>
<div>{% if movie.director %}{% trans '导演:' %}
{% for director in movie.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.genre %}{% trans '类型:' %}
{% for genre in movie.get_genre_display %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.showtime %}{% trans '上映时间:' %}
{% for showtime in movie.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}({{ region }})</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% 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>
<script>
</script>
</body>
</html>

View file

@ -1,160 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}影评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ movie.cover|thumb:'normal' }}">
<title>{{ site_name }}影评 - {{ review.title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link" href="{% url '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 }}
{% csrf_token %}
</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|thumb:'normal' }}" 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 %}">
{% if movie.season %}
{{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season
{{ movie.season }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% else %}
{{ movie.title }} {{ movie.orig_title }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% endif %}
</a>
<a href="{{ movie.source_url }}"><span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span></a>
</h5>
<div>{% if movie.director %}{% trans '导演:' %}
{% for director in movie.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.genre %}{% trans '类型:' %}
{% for genre in movie.get_genre_display %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.showtime %}{% trans '上映时间:' %}
{% for showtime in movie.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}({{ region }})</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% 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>
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -1,169 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ movie.title }}{% trans '的评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url '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.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
{% if review.movie != movie %}
<span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'movies:retrieve' review.movie.id %}">{{ review.movie.get_source_site_display }}</a></span>
{% endif %}
<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|thumb:'normal' }}" 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 %}">
{% if movie.season %}
{{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season
{{ movie.season }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% else %}
{{ movie.title }} {{ movie.orig_title }}
{% if movie.year %}({{ movie.year }}){% endif %}
{% endif %}
</a>
<a href="{{ movie.source_url }}"><span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span></a>
</h5>
<div>{% if movie.director %}{% trans '导演:' %}
{% for director in movie.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if movie.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if movie.genre %}{% trans '类型:' %}
{% for genre in movie.get_genre_display %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if movie.showtime %}{% trans '上映时间:' %}
{% for showtime in movie.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}({{ region }})</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% 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>
<script>
</script>
</body>
</html>

View file

@ -1,109 +0,0 @@
{% 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>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.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" 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.media }}
{% for field in form %}
{% if field.id_for_label == 'id_is_series' %}
<label for="{{ field.id_for_label }}" style="display: inline-block; position: relative; left: -4px;">{{ field.label }}</label>
{{ field }}
{% else %}
{% if field.id_for_label != 'id_id' %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
</form>
<a href="#" 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>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -1,24 +0,0 @@
from django.urls import path, re_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('rescrape/<int:id>/', rescrape, name='rescrape'),
path('mark/', create_update_mark, name='create_update_mark'),
path('wish/<int:id>/', wish, name='wish'),
re_path('(?P<movie_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', 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'),
]

View file

@ -1,584 +0,0 @@
import logging
from django.shortcuts import render, get_object_or_404, redirect, reverse
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse
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 mastodon import mastodon_request_included
from mastodon.models import MastodonApplication
from mastodon.api import share_mark, share_review
from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
from common.models import SourceSiteEnum
from .models import *
from .forms import *
from django.conf import settings
from collection.models import CollectionItem
from common.scraper import get_scraper_by_url, get_normalized_url
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"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
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
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
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"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
else:
return redirect(reverse("users:login"))
else:
return HttpResponseBadRequest()
@login_required
def rescrape(request, id):
if request.method != 'POST':
return HttpResponseBadRequest()
item = get_object_or_404(Movie, pk=id)
url = get_normalized_url(item.source_url)
scraper = get_scraper_by_url(url)
scraper.scrape(url)
form = scraper.save(request_user=request.user, instance=item)
return redirect(reverse("movies:retrieve", args=[form.instance.id]))
@login_required
def update(request, id):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=id)
form = MovieForm(instance=movie)
page_title = _('修改剧集') if movie.is_series else _("修改电影")
return render(
request,
'movies/create_update.html',
{
'form': form,
'is_update': True,
'title': page_title,
'submit_url': reverse("movies:update", args=[movie.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
elif request.method == 'POST':
movie = get_object_or_404(Movie, pk=id)
form = MovieForm(request.POST, request.FILES, instance=movie)
page_title = _('修改剧集') if movie.is_series else _("修改电影")
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
else:
return render(
request,
'movies/create_update.html',
{
'form': form,
'is_update': True,
'title': page_title,
'submit_url': reverse("movies:update", args=[movie.id]),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
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.moviemark_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,
'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
'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_for_identicals(movie, request.user)
review_list = MovieReview.get_available_for_identicals(movie, request.user)
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]
all_collections = CollectionItem.objects.filter(movie=movie).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# 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,
'collection_list': collection_list,
}
)
else:
logger.warning('non-GET method at /movies/<id>')
return HttpResponseBadRequest()
@permission_required("movies.delete_movie")
@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:home"))
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 not pk:
movie_id = request.POST.get('movie')
mark = MovieMark.objects.filter(movie_id=movie_id, owner=request.user).first()
if mark:
pk = mark.id
if pk:
mark = get_object_or_404(MovieMark, pk=pk)
if request.user != mark.owner:
return HttpResponseBadRequest()
old_rating = mark.rating
old_tags = mark.moviemark_tags.all()
if mark.status != request.POST.get('status'):
mark.created_time = timezone.now()
# update
form = MovieMarkForm(request.POST, instance=mark)
else:
# create
form = MovieMarkForm(request.POST)
if form.is_valid():
if form.instance.status == MarkStatusEnum.WISH.value or form.instance.rating == 0:
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:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
if form.cleaned_data['share_to_mastodon']:
if not share_mark(form.instance):
return go_relogin(request)
else:
return HttpResponseBadRequest(f"invalid form data {form.errors}")
return redirect(reverse("movies:retrieve", args=[form.instance.movie.id]))
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def wish(request, id):
if request.method == 'POST':
movie = get_object_or_404(Movie, pk=id)
params = {
'owner': request.user,
'status': MarkStatusEnum.WISH,
'visibility': request.user.preference.default_visibility,
'movie': movie,
}
try:
MovieMark.objects.create(**params)
except Exception:
pass
return HttpResponse("✔️")
else:
return HttpResponseBadRequest("invalid method")
@mastodon_request_included
@login_required
def retrieve_mark_list(request, movie_id, following_only=False):
if request.method == 'GET':
movie = get_object_or_404(Movie, pk=movie_id)
queryset = MovieMark.get_available_for_identicals(movie, request.user, following_only=following_only)
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)
if request.user != mark.owner:
return HttpResponseBadRequest()
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 not share_review(form.instance):
return go_relogin(request)
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):
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 not share_review(form.instance):
return go_relogin(request)
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
def retrieve_review(request, id):
if request.method == 'GET':
review = get_object_or_404(MovieReview, pk=id)
if not review.is_visible_to(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_for_identicals(movie, request.user)
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:
return jump_or_scrape(request, url)
else:
return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()

View file

View file

@ -1,12 +0,0 @@
from django.contrib import admin
from .models import *
from simple_history.admin import SimpleHistoryAdmin
admin.site.register(Song, SimpleHistoryAdmin)
admin.site.register(SongMark)
admin.site.register(SongReview)
admin.site.register(SongTag)
admin.site.register(Album, SimpleHistoryAdmin)
admin.site.register(AlbumMark)
admin.site.register(AlbumReview)
admin.site.register(AlbumTag)

View file

@ -1,11 +0,0 @@
from django.apps import AppConfig
class MusicConfig(AppConfig):
name = 'music'
def ready(self):
from common.index import Indexer
from .models import Album, Song
Indexer.update_model_indexable(Album)
Indexer.update_model_indexable(Song)

View file

@ -1,157 +0,0 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _
from .models import *
from common.models import MarkStatusEnum
from common.forms import *
def MusicMarkStatusTranslator(status):
return MusicMarkStatusTranslation[status]
class SongForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
duration = DurationField(required=False)
class Meta:
model = Song
# fields = '__all__'
fields = [
'id',
'title',
'source_site',
'source_url',
'artist',
'release_date',
'duration',
'isrc',
'genre',
'cover',
'album',
'brief',
'other_info',
]
widgets = {
'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}),
'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}),
'cover': PreviewImageInput(),
}
class SongMarkForm(MarkForm):
STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = SongMark
fields = [
'id',
'song',
'status',
'rating',
'text',
'visibility',
]
widgets = {
'song': forms.TextInput(attrs={"hidden": ""}),
}
class SongReviewForm(ReviewForm):
class Meta:
model = SongReview
fields = [
'id',
'song',
'title',
'content',
'visibility'
]
widgets = {
'song': forms.TextInput(attrs={"hidden": ""}),
}
class AlbumForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
duration = DurationField(required=False)
class Meta:
model = Album
# fields = '__all__'
fields = [
'id',
'title',
'source_site',
'source_url',
'artist',
'company',
'release_date',
'duration',
'genre',
'cover',
'brief',
'track_list',
'other_info',
]
widgets = {
'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}),
'company': forms.TextInput(attrs={'placeholder': _("多个发行方使用英文逗号分隔")}),
'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}),
'cover': PreviewImageInput(),
}
class AlbumMarkForm(MarkForm):
STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = AlbumMark
fields = [
'id',
'album',
'status',
'rating',
'text',
'visibility',
]
widgets = {
'album': forms.TextInput(attrs={"hidden": ""}),
}
class AlbumReviewForm(ReviewForm):
class Meta:
model = AlbumReview
fields = [
'id',
'album',
'title',
'content',
'visibility'
]
widgets = {
'album': forms.TextInput(attrs={"hidden": ""}),
}

View file

@ -1,205 +0,0 @@
from django.core.management.base import BaseCommand
from django.core.files.uploadedfile import SimpleUploadedFile
from common.scraper import *
from django.conf import settings
from music.models import Album
from music.forms import AlbumForm
import requests
import re
import filetype
from lxml import html
from PIL import Image
from io import BytesIO
class DoubanPatcherMixin:
@classmethod
def download_page(cls, url, headers):
url = cls.get_effective_url(url)
r = None
error = 'DoubanScrapper: error occured when downloading ' + url
content = None
def get(url, timeout):
nonlocal r
# print('Douban GET ' + url)
try:
r = requests.get(url, timeout=timeout)
except Exception as e:
r = requests.Response()
r.status_code = f"Exception when GET {url} {e}" + url
# print('Douban CODE ' + str(r.status_code))
return r
def check_content():
nonlocal r, error, content
content = None
if r.status_code == 200:
content = r.content.decode('utf-8')
if content.find('关于豆瓣') == -1:
content = None
error = error + 'Content not authentic' # response is garbage
elif re.search('不存在[^<]+</title>', content, re.MULTILINE):
content = None
error = error + 'Not found or hidden by Douban'
else:
error = error + str(r.status_code)
def fix_wayback_links():
nonlocal content
# fix links
content = re.sub(r'href="http[^"]+http', r'href="http', content)
# https://img9.doubanio.com/view/subject/{l|m|s}/public/s1234.jpg
content = re.sub(r'src="[^"]+/(s\d+\.\w+)"',
r'src="https://img9.doubanio.com/view/subject/m/public/\1"', content)
# https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2681329386.jpg
# https://img9.doubanio.com/view/photo/{l|m|s}/public/p1234.webp
content = re.sub(r'src="[^"]+/(p\d+\.\w+)"',
r'src="https://img9.doubanio.com/view/photo/m/public/\1"', content)
# Wayback Machine: get latest available
def wayback():
nonlocal r, error, content
error = error + '\nWayback: '
get('http://archive.org/wayback/available?url=' + url, 10)
if r.status_code == 200:
w = r.json()
if w['archived_snapshots'] and w['archived_snapshots']['closest']:
get(w['archived_snapshots']['closest']['url'], 10)
check_content()
if content is not None:
fix_wayback_links()
else:
error = error + 'No snapshot available'
else:
error = error + str(r.status_code)
# Wayback Machine: guess via CDX API
def wayback_cdx():
nonlocal r, error, content
error = error + '\nWayback: '
get('http://web.archive.org/cdx/search/cdx?url=' + url, 10)
if r.status_code == 200:
dates = re.findall(r'[^\s]+\s+(\d+)\s+[^\s]+\s+[^\s]+\s+\d+\s+[^\s]+\s+\d{5,}',
r.content.decode('utf-8'))
# assume snapshots whose size >9999 contain real content, use the latest one of them
if len(dates) > 0:
get('http://web.archive.org/web/' + dates[-1] + '/' + url, 10)
check_content()
if content is not None:
fix_wayback_links()
else:
error = error + 'No snapshot available'
else:
error = error + str(r.status_code)
def latest():
nonlocal r, error, content
if settings.SCRAPESTACK_KEY is None:
error = error + '\nDirect: '
get(url, 60)
else:
error = error + '\nScraperAPI: '
if settings.SCRAPESTACK_KEY is not None:
dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
elif settings.SCRAPERAPI_KEY is not None:
dl_url = f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}'
get(dl_url, 60)
check_content()
wayback_cdx()
if content is None:
latest()
if content is None:
logger.error(error)
content = '<html />'
return html.fromstring(content)
@classmethod
def download_image(cls, url, item_url=None):
if url is None:
return None, None
raw_img = None
ext = None
dl_url = url
if settings.SCRAPESTACK_KEY is not None:
dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
elif settings.SCRAPERAPI_KEY is not None:
dl_url = f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}'
try:
img_response = requests.get(dl_url, timeout=90)
if img_response.status_code == 200:
raw_img = img_response.content
img = Image.open(BytesIO(raw_img))
img.load() # corrupted image will trigger exception
content_type = img_response.headers.get('Content-Type')
ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension
else:
logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}")
# raise RuntimeError(f"Douban: download image failed {img_response.status_code} {dl_url}")
except Exception as e:
raw_img = None
ext = None
logger.error(f"Douban: download image failed {e} {dl_url} {item_url}")
if raw_img is None and settings.SCRAPESTACK_KEY is not None:
try:
img_response = requests.get(dl_url, timeout=90)
if img_response.status_code == 200:
raw_img = img_response.content
img = Image.open(BytesIO(raw_img))
img.load() # corrupted image will trigger exception
content_type = img_response.headers.get('Content-Type')
ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension
else:
logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}")
except Exception as e:
raw_img = None
ext = None
logger.error(f"Douban: download image failed {e} {dl_url} {item_url}")
return raw_img, ext
class DoubanAlbumPatcher(DoubanPatcherMixin, AbstractScraper):
site_name = SourceSiteEnum.DOUBAN.value
host = 'music.douban.com'
data_class = Album
form_class = AlbumForm
regex = re.compile(r"https://music\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
headers['Host'] = self.host
content = self.download_page(url, headers)
img_url_elem = content.xpath("//div[@id='mainpic']//img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img, ext = self.download_image(img_url, url)
return raw_img, ext
class Command(BaseCommand):
help = 'fix cover image'
def add_arguments(self, parser):
parser.add_argument('threadId', type=int, help='% 8')
def handle(self, *args, **options):
t = int(options['threadId'])
for m in Album.objects.filter(cover='album/default.svg', source_site='douban'):
if m.id % 8 == t:
self.stdout.write(f'Re-fetching {m.source_url}')
try:
raw_img, img_ext = DoubanAlbumPatcher.scrape(m.source_url)
if img_ext is not None:
m.cover = SimpleUploadedFile('temp.' + img_ext, raw_img)
m.save()
self.stdout.write(self.style.SUCCESS(f'Saved {m.source_url}'))
else:
self.stdout.write(self.style.ERROR(f'Skipped {m.source_url}'))
except Exception as e:
print(e)

View file

@ -1,270 +0,0 @@
import uuid
import django.contrib.postgres.fields as postgres
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import reverse
from common.models import Entity, Mark, Review, Tag, SourceSiteEnum, MarkStatusEnum
from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
from django.utils import timezone
from django.conf import settings
from simple_history.models import HistoricalRecords
MusicMarkStatusTranslation = {
MarkStatusEnum.DO.value: _("在听"),
MarkStatusEnum.WISH.value: _("想听"),
MarkStatusEnum.COLLECT.value: _("听过")
}
def song_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.SONG_MEDIA_PATH_ROOT)
def album_cover_path(instance, filename):
return GenerateDateUUIDMediaFilePath(instance, filename, settings.ALBUM_MEDIA_PATH_ROOT)
class Album(Entity):
title = models.CharField(_("标题"), max_length=500)
release_date = models.DateField(
_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
cover = models.ImageField(
_("封面"), upload_to=album_cover_path, default=settings.DEFAULT_ALBUM_IMAGE, blank=True)
duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
artist = postgres.ArrayField(
models.CharField(_("artist"), blank=True,
default='', max_length=200),
null=True,
blank=True,
default=list,
verbose_name=_("艺术家")
)
genre = models.CharField(_("流派"), blank=True,
default='', max_length=100)
company = postgres.ArrayField(
models.CharField(blank=True,
default='', max_length=500),
null=True,
blank=True,
default=list,
verbose_name=_("发行方")
)
track_list = models.TextField(_("曲目"), blank=True, default="")
history = HistoricalRecords()
@property
def year(self):
return self.release_date.year if self.release_date else None
def __str__(self):
return self.title
def get_json(self):
r = {
'artist': self.artist,
'release_date': self.release_date,
'genre': self.genre,
'publisher': self.company,
}
r.update(super().get_json())
return r
def get_embed_link(self):
if self.source_site == SourceSiteEnum.SPOTIFY.value:
return self.source_url.replace("open.spotify.com/", "open.spotify.com/embed/")
elif self.source_site == SourceSiteEnum.BANDCAMP.value and self.other_info and 'bandcamp_album_id' in self.other_info:
return f"https://bandcamp.com/EmbeddedPlayer/album={self.other_info['bandcamp_album_id']}/size=large/bgcol=ffffff/linkcol=19A2CA/artwork=small/transparent=true/"
else:
return None
def get_absolute_url(self):
return reverse("music:retrieve_album", args=[self.id])
@property
def wish_url(self):
return reverse("music:wish_album", args=[self.id])
def get_tags_manager(self):
return self.album_tags
@property
def verbose_category_name(self):
return _("专辑")
@property
def mark_class(self):
return AlbumMark
@property
def tag_class(self):
return AlbumTag
class Song(Entity):
'''
Song(track) entity, can point to entity Album
'''
title = models.CharField(_("标题"), max_length=500)
release_date = models.DateField(_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
isrc = models.CharField(_("ISRC"),
blank=True, max_length=15, db_index=True, default='')
# duration in ms
duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
cover = models.ImageField(
_("封面"), upload_to=song_cover_path, default=settings.DEFAULT_SONG_IMAGE, blank=True)
artist = postgres.ArrayField(
models.CharField(blank=True,
default='', max_length=100),
null=True,
blank=True,
default=list,
verbose_name=_("艺术家")
)
genre = models.CharField(_("流派"), blank=True, default='', max_length=100)
album = models.ForeignKey(
Album, models.SET_NULL, "album_songs", null=True, blank=True, verbose_name=_("所属专辑"))
history = HistoricalRecords()
def __str__(self):
return self.title
def get_json(self):
r = {
'artist': self.artist,
'release_date': self.release_date,
'genre': self.genre,
}
r.update(super().get_json())
return r
def get_embed_link(self):
return self.source_url.replace("open.spotify.com/", "open.spotify.com/embed/") if self.source_site == SourceSiteEnum.SPOTIFY.value else None
def get_absolute_url(self):
return reverse("music:retrieve_song", args=[self.id])
@property
def wish_url(self):
return reverse("music:wish_song", args=[self.id])
def get_tags_manager(self):
return self.song_tags
@property
def verbose_category_name(self):
return _("单曲")
@property
def mark_class(self):
return SongMark
@property
def tag_class(self):
return SongTag
class SongMark(Mark):
song = models.ForeignKey(
Song, on_delete=models.CASCADE, related_name='song_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'song'], name='unique_song_mark')
]
@property
def translated_status(self):
return MusicMarkStatusTranslation[self.status]
class SongReview(Review):
song = models.ForeignKey(
Song, on_delete=models.CASCADE, related_name='song_reviews', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'song'], name='unique_song_review')
]
@property
def url(self):
return reverse("music:retrieve_song_review", args=[self.id])
@property
def item(self):
return self.song
class SongTag(Tag):
song = models.ForeignKey(
Song, on_delete=models.CASCADE, related_name='song_tags', null=True)
mark = models.ForeignKey(
SongMark, on_delete=models.CASCADE, related_name='songmark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_songmark_tag")
]
@property
def item(self):
return self.song
class AlbumMark(Mark):
album = models.ForeignKey(
Album, on_delete=models.CASCADE, related_name='album_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'album'], name='unique_album_mark')
]
@property
def translated_status(self):
return MusicMarkStatusTranslation[self.status]
class AlbumReview(Review):
album = models.ForeignKey(
Album, on_delete=models.CASCADE, related_name='album_reviews', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'album'], name='unique_album_review')
]
@property
def url(self):
return reverse("music:retrieve_album_review", args=[self.id])
@property
def item(self):
return self.album
class AlbumTag(Tag):
album = models.ForeignKey(
Album, on_delete=models.CASCADE, related_name='album_tags', null=True)
mark = models.ForeignKey(
AlbumMark, on_delete=models.CASCADE, related_name='albummark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_albummark_tag")
]
@property
def item(self):
return self.album

View file

@ -1,457 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}音乐 - {{ album.title }}">
<meta property="og:type" content="music.album">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ album.cover.url }}">
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:description" content="{{ album.brief }}">
<title>{{ site_name }} - {% trans '音乐详情' %} | {{ album.title }}</title>
{% include "partial/_common_libs.html" with jquery=1 %}
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/detail.js' %}"></script>
</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">
<a href="{{ album.cover.url }}" class="entity-detail__img-origin" target="_blank" title="{% trans '查看原图' %}">
<img src="{{ album.cover|thumb:'normal' }}" class="entity-detail__img" alt="{{ album.title }}">
</a>
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ album.title }}
<a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if album.rating and album.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ album.rating }} </span>
<small>({{ album.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if album.company %}{% trans '发行方:' %}
{% for company in album.company %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="company">{{ company }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.company|length > 5 %}
<a href="javascript:void(0);" id="companyMore">{% trans '更多' %}</a>
<script>
$("#companyMore").on('click', function (e) {
$("span.company:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if album.release_date %}
{% trans '发行日期:' %}{{ album.release_date }}
{% endif %}
</div>
<div>{% if album.duration %}
{% trans '时长:' %}{{ album.get_duration_display }}
{% endif %}
</div>
<div>{% if album.genre %}
{% trans '流派:' %}{{ album.genre }}
{% endif %}
</div>
</div>
<div class="entity-detail__fields">
{% if album.other_info %}
{% for k, v in album.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if album.last_editor and album.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.mastodon_username %}">{{ album.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'music:update_album' album.id %}">{% trans '编辑这个作品' %}</a>
{% if user.is_staff %}
/<a href="{% url 'music:delete_album' album.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in album_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>
{% if album.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<p class="entity-desc__content">{{ album.brief | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
{% if album.track_list %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '曲目' %}</h5>
<p class="entity-desc__content">{{ album.track_list | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
{% if album.album_songs.count %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '关联单曲' %}</h5>
<!-- TODO: Limit the maximum -->
<div class="track-carousel">
<div class="track-carousel__content">
{% for song in album.album_songs.all %}
<div class="track-carousel__track">
<a href="{% url 'music:retrieve_song' song.id %}">
<img src="{{ song.cover|thumb:'normal' }}" alt="{{ song }}" class="track-carousel__track-image">
<span class="track-carousel__track-title">
{{ song }}
</span>
</a>
</div>
{% endfor %}
</div>
<!-- <div class="track-carousel__button track-carousel__button--prev">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M15.61 7.41L14.2 6l-6 6 6 6 1.41-1.41L11.03 12l4.58-4.59z" />
</svg>
</div>
<div class="track-carousel__button track-carousel__button--next">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z" />
</svg>
</div> -->
</div>
</div>
{% endif %}
<div class="entity-marks">
<h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
{% if mark_list_more %}
<a href="{% url 'music:retrieve_album_mark_list' album.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
{% endif %}
<a href="{% url 'music:retrieve_album_mark_list' album.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
{% include "partial/mark_list.html" with mark_list=mark_list current_item=album %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'music:retrieve_album_review_list' album.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.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'music:retrieve_album_review' others_review.id %}">{{ others_review.title }}</a></span>
<span>{{ others_review.get_plain_content | truncate:100 }}</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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'music:delete_album_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.created_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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">
<a href="{% url 'music:update_album_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'music:delete_album_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'music:retrieve_album_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 'music:create_album_review' album.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
{% if collection_list %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关收藏单' %}</div>
<div >
{% for c in collection_list %}
<p>
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
</p>
{% endfor %}
<div class="action-panel__button-group action-panel__button-group--center">
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'album' album.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% if album.get_embed_link %}
<iframe src="{{ album.get_embed_link }}" height="320" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
{% endif %}
</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 'music:create_update_album_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.album }}
{% 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.visibility.label }}:</span>
{{ mark_form.visibility }}
</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

@ -1,133 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ album.title }}{% trans '的标记' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'music:retrieve_album' album.id %}">{{ album.title }}</a>{% trans '的标记' %}
</h5>
{% include "partial/mark_list.html" with mark_list=marks current_item=album %}
</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 'music:retrieve_album' album.id %}"><img src="{{ album.cover|thumb:'normal' }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}"><span
class="source-label source-label__{{ album.source_site }}">
{{ album.get_source_site_display }}</span></a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ album.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ album.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,151 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}乐评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ album.cover|thumb:'normal' }}">
<title>{{ site_name }}乐评 - {{ review.title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link"
href="{% url 'music:update_album_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link"
href="{% url 'music:delete_album_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
{% csrf_token %}
</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 'music:retrieve_album' album.id %}"><img src="{{ album.cover|thumb:'normal' }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}">
<span class="source-label source-label__{{ album.source_site }}">
{{ album.get_source_site_display }}
</span>
</a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ album.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ album.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -1,160 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ album.title }}{% trans '的评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'music:retrieve_album' album.id %}">{{ album.title }}</a>{% trans '的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<a href="{% url 'users:home' review.owner.mastodon_username %}"
class="entity-reviews__owner-link">{{ review.owner.username }}</a>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
<span href="{% url 'music:retrieve_album_review' review.id %}"
class="entity-reviews__review-title"><a
href="{% url 'music:retrieve_album_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 'music:retrieve_album' album.id %}"><img src="{{ album.cover|thumb:'normal' }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}"><span
class="source-label source-label__{{ album.source_site }}">
{{ album.get_source_site_display }}</span></a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ album.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ album.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,103 +0,0 @@
{% 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>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
{% if is_update and form.source_site.value != 'in-site' %}
<div style="float:right;padding-left:16px">
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
<div class="action-panel__button-group">
<form method="post" action="{% url 'music:rescrape' form.id.value %}">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
</form>
</div>
</div>
</div>
</div>
{% endif %}
<div class="single-section-wrapper" id="main">
{% comment %} <a href="{% url 'music:scrape_album' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
value="{{ form.instance.release_date | date:"Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -1,126 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/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.min.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 'music:retrieve_album' album.id %}">
<img src="{{ album.cover|thumb:'normal' }}" 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 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ album.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.album }}
<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.visibility.label }}{{ form.visibility }}
</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>
<script>
</script>
</body>
</html>

View file

@ -1,91 +0,0 @@
{% 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>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
<div class="single-section-wrapper" id="main">
{% comment %}
<a href="{% url 'music:scrape_song' %}"
class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}
</a>
{% endcomment %}
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
value="{{ form.instance.release_date | date:"Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -1,130 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/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.min.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 'music:retrieve_song' song.id %}">
<img src="{{ song.cover|thumb:'normal' }}" 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 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}</div>
{% if song.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ song.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.song }}
<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.visibility.label }}{{ form.visibility }}
</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>
<script>
</script>
</body>
</html>

View file

@ -1,99 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '删除音乐' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_album' album.id %}">
<img src="{{ album.cover|thumb:'normal' }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'music:retrieve_album' album.id %}">
<h5 class="entity-card__title">
{{ album.title }}
<a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a>
</h5>
</a>
{% if album.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ album.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if album.last_editor %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' album.last_editor.mastodon_username %}">
<span>{{ album.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ album.edited_time }}</div>
{% if album.album_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ album.album_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if album.album_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ album.album_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_album' album.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>
<script>
</script>
</body>
</html>

View file

@ -1,101 +0,0 @@
{% 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>{{ site_name }} - {% trans '删除评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_album_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>
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -1,99 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '删除音乐' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_song' song.id %}">
<img src="{{ song.cover|thumb:'normal' }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'music:retrieve_song' song.id %}">
<h5 class="entity-card__title">
{{ song.title }}
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
</a>
{% if song.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ song.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if song.last_editor %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' song.last_editor.mastodon_username %}">
<span>{{ song.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ song.edited_time }}</div>
{% if song.song_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ song.song_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if song.song_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ song.song_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_song' song.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>
<script>
</script>
</body>
</html>

View file

@ -1,101 +0,0 @@
{% 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>{{ site_name }} - {% trans '删除评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_song_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>
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -1,112 +0,0 @@
{% 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>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.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" src="https://search.douban.com/music/subject_search{% if q %}?search_text={{ q }}{% endif %}" frameborder="0"></iframe>
<div class="dividing-line"></div>
<div id="scrapeForm">
<form action="{% url 'music:create_album' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}" value="{{ form.instance.release_date | date:"
Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
</form>
<a href="#" 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 'music:click_to_scrape_album' %}" method="post">
{% csrf_token %}
<input type="text" name="url" required placeholder="https://music.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>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -1,109 +0,0 @@
{% 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>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.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.media }}
{% for field in form %}
{% if field.id_for_label == 'id_is_series' %}
<label for="{{ field.id_for_label }}" style="display: inline-block; position: relative; left: -4px;">{{ field.label }}</label>
{{ field }}
{% else %}
{% if field.id_for_label != 'id_id' %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
</form>
<a href="#" 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>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -1,403 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}音乐 - {{ song.title }}">
<meta property="og:type" content="music.song">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ song.cover.url }}">
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:description" content="{{ song.brief }}">
<title>{{ site_name }} - {% trans '音乐详情' %} | {{ song.title }}</title>
{% include "partial/_common_libs.html" with jquery=1 %}
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/detail.js' %}"></script>
</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">
<a href="{{ song.cover.url }}" class="entity-detail__img-origin" target="_blank" title="{% trans '查看原图' %}">
<img src="{{ song.cover|thumb:'normal' }}" class="entity-detail__img" alt="{{ song.title }}">
</a>
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ song.title }}
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if song.rating and song.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ song.rating }} </span>
<small>({{ song.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if song.release_date %}
{% trans '发行日期:' %}{{ song.release_date }}
{% endif %}
</div>
<div>{% if song.duration %}
{% trans '时长:' %}{{ song.get_duration_display }}
{% endif %}
</div>
<div>{% if song.genre %}
{% trans '流派:' %}{{ song.genre }}
{% endif %}
</div>
</div>
<div class="entity-detail__fields">
<div>{% if song.isrc %}
{% trans 'ISRC' %}{{ song.isrc }}
{% endif %}
</div>
<div>{% if song.album %}
{% trans '所属专辑:' %}<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
{% if song.other_info %}
{% for k, v in song.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if song.last_editor and song.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.mastodon_username %}">{{ song.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'music:update_song' song.id %}">{% trans '编辑这个作品' %}</a>
{% if user.is_staff %}
/<a href="{% url 'music:delete_song' song.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in song_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>
{% if song.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<p class="entity-desc__content">{{ song.brief | 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 'music:retrieve_song_mark_list' song.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
{% endif %}
<a href="{% url 'music:retrieve_song_mark_list' song.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
{% include "partial/mark_list.html" with mark_list=mark_list current_item=song %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'music:retrieve_song_review_list' song.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.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'music:retrieve_song_review' others_review.id %}">{{ others_review.title }}</a></span>
<span>{{ others_review.get_plain_content | truncate:100 }}</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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'music:delete_song_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.created_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.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">
<a href="{% url 'music:update_song_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'music:delete_song_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'music:retrieve_song_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 'music:create_song_review' song.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
{% if collection_list %}
<div class="aside-section-wrapper">
<div class="action-panel">
<div class="action-panel__label">{% trans '相关收藏单' %}</div>
<div >
{% for c in collection_list %}
<p>
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
</p>
{% endfor %}
<div class="action-panel__button-group action-panel__button-group--center">
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'song' song.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% if song.source_site == "spotify" %}
<iframe src="{{ song.get_embed_link }}" height="80" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
{% endif %}
</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 'music:create_update_song_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.song }}
{% 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.visibility.label }}:</span>
{{ mark_form.visibility }}
</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

@ -1,137 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ song.title }}{% trans '的标记' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans '的标记' %}
</h5>
{% include "partial/mark_list.html" with mark_list=marks current_item=song %}
</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 'music:retrieve_song' song.id %}"><img src="{{ song.cover|thumb:'normal' }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span
class="source-label source-label__{{ song.source_site }}">{{song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
</div>
{% if song.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ song.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ song.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,147 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="{{ site_name }}乐评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ song.cover|thumb:'normal' }}">
<title>{{ site_name }}乐评 - {{ review.title }}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>
{% endif %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link" href="{% url 'music:update_song_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link" href="{% url 'music:delete_song_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
{% csrf_token %}
</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 'music:retrieve_song' song.id %}"><img src="{{ song.cover|thumb:'normal' }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}</div>
{% if song.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ song.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -1,152 +0,0 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {{ song.title }}{% trans '的评论' %}</title>
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans ' 的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
{% if review.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
<span href="{% url 'music:retrieve_song_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'music:retrieve_song_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 'music:retrieve_song' song.id %}"><img src="{{ song.cover|thumb:'normal' }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}</div>
{% if song.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ song.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
</script>
</body>
</html>

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,41 +0,0 @@
from django.urls import path, re_path
from .views import *
app_name = 'music'
urlpatterns = [
path('song/create/', create_song, name='create_song'),
path('song/<int:id>/', retrieve_song, name='retrieve_song'),
path('song/update/<int:id>/', update_song, name='update_song'),
path('song/delete/<int:id>/', delete_song, name='delete_song'),
path('song/mark/', create_update_song_mark, name='create_update_song_mark'),
path('song/wish/<int:id>/', wish_song, name='wish_song'),
path('song/<int:song_id>/mark/list/',
retrieve_song_mark_list, name='retrieve_song_mark_list'),
path('song/mark/delete/<int:id>/', delete_song_mark, name='delete_song_mark'),
path('song/<int:song_id>/review/create/', create_song_review, name='create_song_review'),
path('song/review/update/<int:id>/', update_song_review, name='update_song_review'),
path('song/review/delete/<int:id>/', delete_song_review, name='delete_song_review'),
path('song/review/<int:id>/', retrieve_song_review, name='retrieve_song_review'),
re_path('song/(?P<song_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_song_mark_list, name='retrieve_song_mark_list'),
# path('song/scrape/', scrape_song, name='scrape_song'),
path('song/click_to_scrape/', click_to_scrape_song, name='click_to_scrape_song'),
path('album/create/', create_album, name='create_album'),
path('album/<int:id>/', retrieve_album, name='retrieve_album'),
path('album/update/<int:id>/', update_album, name='update_album'),
path('album/delete/<int:id>/', delete_album, name='delete_album'),
path('rescrape/<int:id>/', rescrape, name='rescrape'),
path('album/mark/', create_update_album_mark, name='create_update_album_mark'),
path('album/wish/<int:id>/', wish_album, name='wish_album'),
re_path('album/(?P<album_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_album_mark_list, name='retrieve_album_mark_list'),
path('album/mark/delete/<int:id>/', delete_album_mark, name='delete_album_mark'),
path('album/<int:album_id>/review/create/', create_album_review, name='create_album_review'),
path('album/review/update/<int:id>/', update_album_review, name='update_album_review'),
path('album/review/delete/<int:id>/', delete_album_review, name='delete_album_review'),
path('album/review/<int:id>/', retrieve_album_review, name='retrieve_album_review'),
path('album/<int:album_id>/review/list/',
retrieve_album_review_list, name='retrieve_album_review_list'),
path('album/scrape/', scrape_album, name='scrape_album'),
path('album/click_to_scrape/', click_to_scrape_album, name='click_to_scrape_album'),
]

File diff suppressed because it is too large Load diff