remove pre-0.5 apps
This commit is contained in:
parent
b5632f4844
commit
3439fb93e8
98 changed files with 2 additions and 13437 deletions
8
.github/workflows/django.yml
vendored
8
.github/workflows/django.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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)
|
116
books/forms.py
116
books/forms.py
|
@ -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": ""}),
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
188
books/models.py
188
books/models.py
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ reviews.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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'),
|
||||
]
|
583
books/views.py
583
books/views.py
|
@ -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()
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CollectionConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'collection'
|
|
@ -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(),
|
||||
}
|
|
@ -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 '关注了收藏单'
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ collections.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ collections.pagination.last_page }}" class="pagination__nav-link">»</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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
|
||||
]
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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": ""}),
|
||||
}
|
173
games/models.py
173
games/models.py
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ reviews.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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'),
|
||||
]
|
585
games/views.py
585
games/views.py
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
137
movies/forms.py
137
movies/forms.py
|
@ -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": ""}),
|
||||
}
|
|
@ -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
|
293
movies/models.py
293
movies/models.py
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ reviews.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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'),
|
||||
]
|
584
movies/views.py
584
movies/views.py
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
157
music/forms.py
157
music/forms.py
|
@ -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": ""}),
|
||||
}
|
|
@ -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)
|
270
music/models.py
270
music/models.py
|
@ -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
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ reviews.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ reviews.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}"
|
||||
class="pagination__nav-link">»</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>
|
|
@ -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>
|
|
@ -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">«</a>
|
||||
<a href="?page={{ reviews.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</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">›</a>
|
||||
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">»</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>
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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'),
|
||||
]
|
1154
music/views.py
1154
music/views.py
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue