commit
8a5897b4c3
67 changed files with 6170 additions and 315 deletions
|
@ -25,7 +25,13 @@ urlpatterns = [
|
|||
path('users/', include('users.urls')),
|
||||
path('books/', include('books.urls')),
|
||||
path('movies/', include('movies.urls')),
|
||||
path('music/', include('music.urls')),
|
||||
path('announcement/', include('management.urls')),
|
||||
path('', include('common.urls')),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.conf.urls.static import static
|
||||
urlpatterns += static(settings.MEDIA_URL,
|
||||
document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
@ -77,42 +77,16 @@ class BookForm(forms.ModelForm):
|
|||
return isbn
|
||||
|
||||
|
||||
class BookMarkForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
STATUS_CHOICES = [(v, BookMarkStatusTranslator(v)) for v in MarkStatusEnum.values]
|
||||
class BookMarkForm(MarkForm):
|
||||
|
||||
STATUS_CHOICES = [(v, BookMarkStatusTranslator(v))
|
||||
for v in MarkStatusEnum.values]
|
||||
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
share_to_mastodon = forms.BooleanField(label=_("分享到长毛象"), initial=True, required=False)
|
||||
rating = forms.IntegerField(validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
|
||||
status = forms.ChoiceField(
|
||||
label=_(""),
|
||||
widget=forms.RadioSelect(),
|
||||
choices=STATUS_CHOICES
|
||||
)
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
tags = TagField(
|
||||
required=False,
|
||||
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
|
||||
label = _("标签")
|
||||
)
|
||||
text = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"placeholder": _("最多只能写360字哦~"),
|
||||
"maxlength": 360
|
||||
}
|
||||
),
|
||||
|
||||
label = _("短评"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = BookMark
|
||||
|
@ -132,18 +106,8 @@ class BookMarkForm(forms.ModelForm):
|
|||
}
|
||||
|
||||
|
||||
class BookReviewForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
share_to_mastodon = forms.BooleanField(label=_("分享到长毛象"), initial=True, required=False)
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
class BookReviewForm(ReviewForm):
|
||||
|
||||
class Meta:
|
||||
model = BookReview
|
||||
fields = [
|
||||
|
|
|
@ -47,7 +47,7 @@ class Book(Entity):
|
|||
# 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=True, max_length=20, db_index=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=DEFAULT_BOOK_IMAGE, blank=True)
|
||||
contents = models.TextField(blank=True, default="")
|
||||
|
|
|
@ -52,9 +52,12 @@
|
|||
{% endif %}
|
||||
|
||||
{% if book.last_editor %}
|
||||
<a href="{% url 'users:home' book.last_editor.id %}">
|
||||
<div>{% trans '最近编辑者:' %}{{ book.last_editor | default:"" }}</div>
|
||||
</a>
|
||||
<div>
|
||||
{% trans '最近编辑者:' %}
|
||||
<a href="{% url 'users:home' book.last_editor.id %}">
|
||||
<span>{{ book.last_editor | default:"" }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>{% trans '上次编辑时间:' %}{{ book.edited_time }}</div>
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
<div>
|
||||
<a href="{% url 'books:update' book.id %}">{% trans '编辑这本书' %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'books:delete' book.id %}"> / {% trans '删除' %}</a>
|
||||
/<a href="{% url 'books:delete' book.id %}"> {% trans '删除' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -120,20 +120,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if book.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
{% if book.brief %}
|
||||
|
||||
<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>
|
||||
|
||||
{% else %}
|
||||
<div>{% trans '暂无简介' %}</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if book.contents %}
|
||||
<div class="entity-desc" id="contents">
|
||||
|
@ -160,7 +158,7 @@
|
|||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
|
@ -184,7 +182,7 @@
|
|||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
|
@ -212,7 +210,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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>
|
||||
|
@ -255,7 +253,7 @@
|
|||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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">
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.db import IntegrityError, transaction
|
|||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from mastodon import mastodon_request_included
|
||||
from mastodon.api import check_visibility, post_toot, TootVisibilityEnum
|
||||
from mastodon.utils import rating_to_emoji
|
||||
|
|
164
common/forms.py
164
common/forms.py
|
@ -7,30 +7,42 @@ import json
|
|||
|
||||
|
||||
class KeyValueInput(forms.Widget):
|
||||
template_name = 'widgets/key_value.html'
|
||||
|
||||
"""
|
||||
Input widget for Json field
|
||||
"""
|
||||
template_name = 'widgets/hstore.html'
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
""" called when rendering """
|
||||
context = {}
|
||||
context['widget'] = {
|
||||
'name': name,
|
||||
'is_hidden': self.is_hidden,
|
||||
'required': self.is_required,
|
||||
'value': self.format_value(value),
|
||||
'attrs': self.build_attrs(self.attrs, attrs),
|
||||
'template_name': self.template_name,
|
||||
'keyvalue_pairs': {},
|
||||
}
|
||||
if context['widget']['value']:
|
||||
key_value_pairs = json.loads(context['widget']['value'])
|
||||
# for kv in key_value_pairs:
|
||||
context['widget']['keyvalue_pairs'] = key_value_pairs
|
||||
context = super().get_context(name, value, attrs)
|
||||
data = json.loads(context['widget']['value'])
|
||||
context['widget']['value'] = [ {p[0]: p[1]} for p in data.items()]
|
||||
return context
|
||||
|
||||
class Media:
|
||||
js = ('js/key_value_input.js',)
|
||||
|
||||
|
||||
class HstoreInput(forms.Widget):
|
||||
"""
|
||||
Input widget for Hstore field
|
||||
"""
|
||||
template_name = 'widgets/hstore.html'
|
||||
|
||||
def format_value(self, value):
|
||||
"""
|
||||
Return a value as it should appear when rendered in a template.
|
||||
"""
|
||||
if value == '' or value is None:
|
||||
return None
|
||||
if self.is_localized:
|
||||
return formats.localize_input(value)
|
||||
# do not return str
|
||||
return value
|
||||
|
||||
class Media:
|
||||
js = ('js/key_value_input.js',)
|
||||
|
||||
|
||||
class JSONField(postgres.JSONField):
|
||||
widget = KeyValueInput
|
||||
def to_python(self, value):
|
||||
|
@ -145,23 +157,6 @@ class MultiSelect(forms.SelectMultiple):
|
|||
js = ('lib/js/multiple-select.min.js',)
|
||||
|
||||
|
||||
class HstoreInput(forms.Widget):
|
||||
template_name = 'widgets/hstore.html'
|
||||
|
||||
def format_value(self, value):
|
||||
"""
|
||||
Return a value as it should appear when rendered in a template.
|
||||
"""
|
||||
if value == '' or value is None:
|
||||
return None
|
||||
if self.is_localized:
|
||||
return formats.localize_input(value)
|
||||
return value
|
||||
|
||||
class Media:
|
||||
js = ('js/key_value_input.js',)
|
||||
|
||||
|
||||
class HstoreField(forms.CharField):
|
||||
widget = HstoreInput
|
||||
def to_python(self, value):
|
||||
|
@ -174,3 +169,104 @@ class HstoreField(forms.CharField):
|
|||
if len(pairs) == 1:
|
||||
pairs = (pairs,)
|
||||
return pairs
|
||||
|
||||
|
||||
class DurationInput(forms.TextInput):
|
||||
"""
|
||||
HH:mm:ss input widget
|
||||
"""
|
||||
input_type = "time"
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
# context['widget']['type'] = self.input_type
|
||||
context['widget']['attrs']['step'] = "1"
|
||||
return context
|
||||
|
||||
def format_value(self, value):
|
||||
"""
|
||||
Given `value` is an integer in ms
|
||||
"""
|
||||
ms = value
|
||||
if not ms:
|
||||
return super().format_value(None)
|
||||
x = ms // 1000
|
||||
seconds = x % 60
|
||||
x //= 60
|
||||
if x == 0:
|
||||
return super().format_value(f"00:00:{seconds:0>2}")
|
||||
minutes = x % 60
|
||||
x //= 60
|
||||
if x == 0:
|
||||
return super().format_value(f"00:{minutes:0>2}:{seconds:0>2}")
|
||||
hours = x % 24
|
||||
return super().format_value(f"{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
|
||||
|
||||
|
||||
class DurationField(forms.TimeField):
|
||||
widget = DurationInput
|
||||
def to_python(self, value):
|
||||
|
||||
# empty value
|
||||
if value is None or value == '':
|
||||
return
|
||||
|
||||
# if value is integer in ms
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
# if value is string in time format
|
||||
h, m, s = value.split(':')
|
||||
return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000
|
||||
|
||||
|
||||
#############################
|
||||
# Form
|
||||
#############################
|
||||
|
||||
class MarkForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
share_to_mastodon = forms.BooleanField(
|
||||
label=_("分享到长毛象"), initial=True, required=False)
|
||||
rating = forms.IntegerField(
|
||||
validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
tags = TagField(
|
||||
required=False,
|
||||
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
|
||||
label=_("标签")
|
||||
)
|
||||
text = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"placeholder": _("最多只能写360字哦~"),
|
||||
"maxlength": 360
|
||||
}
|
||||
),
|
||||
|
||||
label=_("短评"),
|
||||
)
|
||||
|
||||
class ReviewForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
share_to_mastodon = forms.BooleanField(
|
||||
label=_("分享到长毛象"), initial=True, required=False)
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ RE_HTML_TAG = re.compile(r"<[^>]*>")
|
|||
class SourceSiteEnum(models.TextChoices):
|
||||
IN_SITE = "in-site", CLIENT_NAME
|
||||
DOUBAN = "douban", _("豆瓣")
|
||||
SPOTIFY = "spotify", _("Spotify")
|
||||
|
||||
|
||||
class Entity(models.Model):
|
||||
|
@ -32,8 +33,8 @@ class Entity(models.Model):
|
|||
edited_time = models.DateTimeField(auto_now_add=True)
|
||||
last_editor = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, related_name='%(class)s_last_editor', null=True, blank=False)
|
||||
brief = models.TextField(blank=True, default="")
|
||||
other_info = postgres.JSONField(
|
||||
brief = models.TextField(_("简介"), blank=True, default="")
|
||||
other_info = postgres.JSONField(_("其他信息"),
|
||||
blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
|
||||
# source_url should include shceme, which is normally https://
|
||||
source_url = models.URLField(_("URL"), max_length=500, unique=True)
|
||||
|
@ -114,7 +115,7 @@ class Entity(models.Model):
|
|||
"""
|
||||
raise NotImplementedError("Subclass should implement this method.")
|
||||
|
||||
def get_revies_manager(self):
|
||||
def get_reviews_manager(self):
|
||||
"""
|
||||
Normally this won't be used.
|
||||
There is no ocassion where visitor can simply view all the reviews.
|
||||
|
|
|
@ -2,15 +2,26 @@ import requests
|
|||
import functools
|
||||
import random
|
||||
import logging
|
||||
from lxml import html
|
||||
import re
|
||||
import dateparser
|
||||
import datetime
|
||||
import time
|
||||
from lxml import html
|
||||
from mimetypes import guess_extension
|
||||
from threading import Thread
|
||||
from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG
|
||||
from boofilsic.settings import SPOTIFY_CREDENTIAL
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from common.models import SourceSiteEnum
|
||||
from movies.models import Movie, MovieGenreEnum
|
||||
from movies.forms import MovieForm
|
||||
from books.models import Book
|
||||
from books.forms import BookForm
|
||||
from music.models import Album, Song
|
||||
from music.forms import AlbumForm, SongForm
|
||||
|
||||
|
||||
RE_NUMBERS = re.compile(r"\d+\d*")
|
||||
|
@ -18,7 +29,7 @@ RE_WHITESPACES = re.compile(r"\s+")
|
|||
|
||||
|
||||
DEFAULT_REQUEST_HEADERS = {
|
||||
'Host': 'book.douban.com',
|
||||
'Host': '',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:70.0) Gecko/20100101 Firefox/70.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
|
||||
|
@ -53,19 +64,23 @@ def log_url(func):
|
|||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# log the url
|
||||
# log the url and trace stack
|
||||
logger.error(f"Scrape Failed URL: {args[1]}")
|
||||
logger.error(str(e))
|
||||
logger.error("Expections during scraping:", exc_info=e)
|
||||
raise e
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class AbstractScraper:
|
||||
"""
|
||||
Scrape entities. The entities means those defined in the models.py file,
|
||||
like Book, Movie......
|
||||
"""
|
||||
|
||||
# subclasses must specify those two variables
|
||||
# site means general sites, like amazon/douban etc
|
||||
site = None
|
||||
site_name = None
|
||||
# host means technically hostname
|
||||
host = None
|
||||
# corresponding data class
|
||||
|
@ -74,34 +89,49 @@ class AbstractScraper:
|
|||
form_class = None
|
||||
# used to extract effective url
|
||||
regex = None
|
||||
# scraped raw image
|
||||
raw_img = None
|
||||
# scraped raw data
|
||||
raw_data = {}
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
# this statement initialize the subclasses
|
||||
super().__init_subclass__(**kwargs)
|
||||
assert cls.site is not None, "class variable `site` must be specified"
|
||||
assert cls.host is not None, "class variable `host` must be specified"
|
||||
assert cls.site_name is not None, "class variable `site_name` must be specified"
|
||||
assert bool(cls.host), "class variable `host` must be specified"
|
||||
assert cls.data_class is not None, "class variable `data_class` must be specified"
|
||||
assert cls.form_class is not None, "class variable `form_class` must be specified"
|
||||
assert cls.regex is not None, "class variable `regex` must be specified"
|
||||
assert isinstance(cls.host, str), "`host` must be type str"
|
||||
assert cls.site in SourceSiteEnum, "`site` must be one of `SourceSiteEnum` value"
|
||||
assert hasattr(cls, 'scrape') and callable(cls.scrape), "scaper must have method `.scrape()`"
|
||||
assert isinstance(cls.host, str) or (isinstance(cls.host, list) and isinstance(
|
||||
cls.host[0], str)), "`host` must be type str or list"
|
||||
assert cls.site_name in SourceSiteEnum, "`site_name` must be one of `SourceSiteEnum` value"
|
||||
assert hasattr(cls, 'scrape') and callable(
|
||||
cls.scrape), "scaper must have method `.scrape()`"
|
||||
|
||||
# decorate the scrape method
|
||||
cls.scrape = classmethod(log_url(cls.scrape))
|
||||
scraper_registry[cls.host] = cls
|
||||
|
||||
|
||||
# register scraper
|
||||
if isinstance(cls.host, list):
|
||||
for host in cls.host:
|
||||
scraper_registry[host] = cls
|
||||
else:
|
||||
scraper_registry[cls.host] = cls
|
||||
|
||||
def scrape(self, url):
|
||||
"""
|
||||
Scrape/request model schema specified data from given url and return it.
|
||||
Implementations of subclasses to this method would be decorated as class method.
|
||||
return (data_dict, image)
|
||||
Should set the `raw_data` and the `raw_img`
|
||||
"""
|
||||
raise NotImplementedError("Subclass should implement this method")
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_effective_url(cls, raw_url):
|
||||
"""
|
||||
The return value should be identical with that saved in DB as `source_url`
|
||||
"""
|
||||
url = cls.regex.findall(raw_url)
|
||||
if not url:
|
||||
raise ValueError("not valid url")
|
||||
|
@ -113,14 +143,15 @@ class AbstractScraper:
|
|||
|
||||
session_id = random.random()
|
||||
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
|
||||
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
|
||||
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
|
||||
proxies = {
|
||||
'http': proxy_url,
|
||||
'https': proxy_url,
|
||||
}
|
||||
# if DEBUG:
|
||||
# proxies = None
|
||||
r = requests.get(url, proxies=proxies, headers=headers, timeout=TIMEOUT)
|
||||
r = requests.get(url, proxies=proxies,
|
||||
headers=headers, timeout=TIMEOUT)
|
||||
# r = requests.get(url, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT)
|
||||
|
||||
return html.fromstring(r.content.decode('utf-8'))
|
||||
|
@ -132,7 +163,7 @@ class AbstractScraper:
|
|||
raw_img = None
|
||||
session_id = random.random()
|
||||
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
|
||||
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
|
||||
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
|
||||
proxies = {
|
||||
'http': proxy_url,
|
||||
'https': proxy_url,
|
||||
|
@ -155,16 +186,34 @@ class AbstractScraper:
|
|||
)
|
||||
if img_response.status_code == 200:
|
||||
raw_img = img_response.content
|
||||
return raw_img
|
||||
content_type = img_response.headers.get('Content-Type')
|
||||
ext = guess_extension(content_type.partition(';')[0].strip())
|
||||
return raw_img, ext
|
||||
|
||||
|
||||
@classmethod
|
||||
def save(cls, request_user):
|
||||
entity_cover = {
|
||||
'cover': SimpleUploadedFile('temp' + cls.img_ext, cls.raw_img)
|
||||
}
|
||||
form = cls.form_class(cls.raw_data, entity_cover)
|
||||
if form.is_valid():
|
||||
form.instance.last_editor = request_user
|
||||
form.save()
|
||||
cls.instance = form.instance
|
||||
else:
|
||||
logger.error(str(form.errors))
|
||||
raise ValidationError("Form invalid.")
|
||||
return form
|
||||
|
||||
|
||||
class DoubanBookScraper(AbstractScraper):
|
||||
site = SourceSiteEnum.DOUBAN.value
|
||||
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}")
|
||||
regex = re.compile(r"https://book\.douban\.com/subject/\d+/{0,1}")
|
||||
|
||||
def scrape(self, url):
|
||||
headers = DEFAULT_REQUEST_HEADERS.copy()
|
||||
|
@ -234,7 +283,8 @@ class DoubanBookScraper(AbstractScraper):
|
|||
|
||||
brief_elem = content.xpath(
|
||||
"//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()")
|
||||
brief = '\n'.join(p.strip() for p in brief_elem) if brief_elem else None
|
||||
brief = '\n'.join(p.strip()
|
||||
for p in brief_elem) if brief_elem else None
|
||||
|
||||
contents = None
|
||||
try:
|
||||
|
@ -253,7 +303,7 @@ class DoubanBookScraper(AbstractScraper):
|
|||
|
||||
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 = self.download_image(img_url)
|
||||
raw_img, ext = self.download_image(img_url)
|
||||
|
||||
# there are two html formats for authors and translators
|
||||
authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
|
||||
|
@ -311,23 +361,24 @@ class DoubanBookScraper(AbstractScraper):
|
|||
'brief': brief,
|
||||
'contents': contents,
|
||||
'other_info': other,
|
||||
'source_site': self.site,
|
||||
'source_site': self.site_name,
|
||||
'source_url': self.get_effective_url(url),
|
||||
}
|
||||
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
|
||||
return data, raw_img
|
||||
|
||||
|
||||
class DoubanMovieScraper(AbstractScraper):
|
||||
site = SourceSiteEnum.DOUBAN.value
|
||||
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}")
|
||||
regex = re.compile(r"https://movie\.douban\.com/subject/\d+/{0,1}")
|
||||
|
||||
def scrape(self, url):
|
||||
headers = DEFAULT_REQUEST_HEADERS.copy()
|
||||
headers['Host'] = 'movie.douban.com'
|
||||
headers['Host'] = self.host
|
||||
content = self.download_page(url, headers)
|
||||
|
||||
# parsing starts here
|
||||
|
@ -461,7 +512,7 @@ class DoubanMovieScraper(AbstractScraper):
|
|||
|
||||
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 = self.download_image(img_url)
|
||||
raw_img, ext = self.download_image(img_url)
|
||||
|
||||
data = {
|
||||
'title': title,
|
||||
|
@ -483,8 +534,387 @@ class DoubanMovieScraper(AbstractScraper):
|
|||
'single_episode_length': single_episode_length,
|
||||
'brief': brief,
|
||||
'is_series': is_series,
|
||||
'source_site': self.site,
|
||||
'source_site': self.site_name,
|
||||
'source_url': self.get_effective_url(url),
|
||||
}
|
||||
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
|
||||
return data, raw_img
|
||||
|
||||
|
||||
|
||||
class DoubanAlbumScraper(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)
|
||||
|
||||
# parsing starts here
|
||||
try:
|
||||
title = content.xpath("//h1/span/text()")[0].strip()
|
||||
except IndexError:
|
||||
raise ValueError("given url contains no album info")
|
||||
if not title:
|
||||
raise ValueError("given url contains no album info")
|
||||
|
||||
|
||||
artists_elem = content.xpath("""//div[@id='info']/span/span[@class='pl']/a/text()""")
|
||||
artist = None if not artists_elem else artists_elem
|
||||
|
||||
genre_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='流派:']/following::text()[1]")
|
||||
genre = genre_elem[0].strip() if genre_elem else None
|
||||
|
||||
date_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
|
||||
release_date = dateparser.parse(date_elem[0].strip(), settings={
|
||||
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)}) if date_elem else None
|
||||
|
||||
company_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='出版者:']/following::text()[1]")
|
||||
company = company_elem[0].strip() if company_elem else None
|
||||
|
||||
track_list_elem = content.xpath(
|
||||
"//div[@class='track-list']/div[@class='indent']/div/text()"
|
||||
)
|
||||
if track_list_elem:
|
||||
track_list = '\n'.join([track.strip() for track in track_list_elem])
|
||||
else:
|
||||
track_list = None
|
||||
|
||||
brief_elem = content.xpath("//span[@class='all hidden']")
|
||||
if not brief_elem:
|
||||
brief_elem = content.xpath("//span[@property='v:summary']")
|
||||
brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
|
||||
'./text()')]) if brief_elem else None
|
||||
|
||||
other_info = {}
|
||||
other_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
|
||||
if other_elem:
|
||||
other_info['又名'] = other_elem[0].strip()
|
||||
other_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
|
||||
if other_elem:
|
||||
other_info['专辑类型'] = other_elem[0].strip()
|
||||
other_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
|
||||
if other_elem:
|
||||
other_info['介质'] = other_elem[0].strip()
|
||||
other_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
|
||||
if other_elem:
|
||||
other_info['ISRC'] = other_elem[0].strip()
|
||||
other_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
|
||||
if other_elem:
|
||||
other_info['条形码'] = other_elem[0].strip()
|
||||
other_elem = content.xpath(
|
||||
"//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
|
||||
if other_elem:
|
||||
other_info['碟片数'] = other_elem[0].strip()
|
||||
|
||||
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)
|
||||
|
||||
data = {
|
||||
'title': title,
|
||||
'artist': artist,
|
||||
'genre': genre,
|
||||
'release_date': release_date,
|
||||
'duration': None,
|
||||
'company': company,
|
||||
'track_list': track_list,
|
||||
'brief': brief,
|
||||
'other_info': other_info,
|
||||
'source_site': self.site_name,
|
||||
'source_url': self.get_effective_url(url),
|
||||
}
|
||||
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
|
||||
return data, raw_img
|
||||
|
||||
|
||||
spotify_token = None
|
||||
spotify_token_expire_time = time.time()
|
||||
|
||||
class SpotifyTrackScraper(AbstractScraper):
|
||||
site_name = SourceSiteEnum.SPOTIFY.value
|
||||
# API URL
|
||||
host = 'https://open.spotify.com/track/'
|
||||
data_class = Song
|
||||
form_class = SongForm
|
||||
|
||||
regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+")
|
||||
|
||||
def scrape(self, url):
|
||||
"""
|
||||
Request from API, not really scraping
|
||||
"""
|
||||
global spotify_token, spotify_token_expire_time
|
||||
|
||||
if spotify_token is None or is_spotify_token_expired():
|
||||
invoke_spotify_token()
|
||||
effective_url = self.get_effective_url(url)
|
||||
if effective_url is None:
|
||||
raise ValueError("not valid url")
|
||||
|
||||
api_url = self.get_api_url(effective_url)
|
||||
headers = {
|
||||
'Authorization': f"Bearer {spotify_token}"
|
||||
}
|
||||
r = requests.get(api_url, headers=headers)
|
||||
res_data = r.json()
|
||||
|
||||
artist = []
|
||||
for artist_dict in res_data['artists']:
|
||||
artist.append(artist_dict['name'])
|
||||
if not artist:
|
||||
artist = None
|
||||
|
||||
title = res_data['name']
|
||||
|
||||
release_date = dateparser.parse(
|
||||
res_data['album']['release_date'],
|
||||
settings={
|
||||
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)
|
||||
}
|
||||
)
|
||||
|
||||
duration = res_data['duration_ms']
|
||||
|
||||
if res_data['external_ids'].get('isrc'):
|
||||
isrc = res_data['external_ids']['isrc']
|
||||
else:
|
||||
isrc = None
|
||||
|
||||
raw_img, ext = self.download_image(res_data['album']['images'][0]['url'])
|
||||
|
||||
data = {
|
||||
'title': title,
|
||||
'artist': artist,
|
||||
'genre': None,
|
||||
'release_date': release_date,
|
||||
'duration': duration,
|
||||
'isrc': isrc,
|
||||
'album': None,
|
||||
'brief': None,
|
||||
'other_info': None,
|
||||
'source_site': self.site_name,
|
||||
'source_url': effective_url,
|
||||
}
|
||||
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
|
||||
return data, raw_img
|
||||
|
||||
@classmethod
|
||||
def get_effective_url(cls, raw_url):
|
||||
code = cls.regex.findall(raw_url)
|
||||
if code:
|
||||
return f"https://open.spotify.com/track/{code[0]}"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_api_url(cls, url):
|
||||
return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0]
|
||||
|
||||
|
||||
class SpotifyAlbumScraper(AbstractScraper):
|
||||
site_name = SourceSiteEnum.SPOTIFY.value
|
||||
# API URL
|
||||
host = 'https://open.spotify.com/album/'
|
||||
data_class = Album
|
||||
form_class = AlbumForm
|
||||
|
||||
regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+")
|
||||
|
||||
def scrape(self, url):
|
||||
"""
|
||||
Request from API, not really scraping
|
||||
"""
|
||||
global spotify_token, spotify_token_expire_time
|
||||
|
||||
if spotify_token is None or is_spotify_token_expired():
|
||||
invoke_spotify_token()
|
||||
effective_url = self.get_effective_url(url)
|
||||
if effective_url is None:
|
||||
raise ValueError("not valid url")
|
||||
|
||||
api_url = self.get_api_url(effective_url)
|
||||
headers = {
|
||||
'Authorization': f"Bearer {spotify_token}"
|
||||
}
|
||||
r = requests.get(api_url, headers=headers)
|
||||
res_data = r.json()
|
||||
|
||||
artist = []
|
||||
for artist_dict in res_data['artists']:
|
||||
artist.append(artist_dict['name'])
|
||||
|
||||
title = res_data['name']
|
||||
|
||||
genre = ', '.join(res_data['genres'])
|
||||
|
||||
company = []
|
||||
for com in res_data['copyrights']:
|
||||
company.append(com['text'])
|
||||
|
||||
duration = 0
|
||||
track_list = []
|
||||
track_urls = []
|
||||
for track in res_data['tracks']['items']:
|
||||
track_urls.append(track['external_urls']['spotify'])
|
||||
duration += track['duration_ms']
|
||||
if res_data['tracks']['items'][-1]['disc_number'] > 1:
|
||||
# more than one disc
|
||||
track_list.append(str(
|
||||
track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name'])
|
||||
else:
|
||||
track_list.append(str(track['track_number']) + '. ' + track['name'])
|
||||
track_list = '\n'.join(track_list)
|
||||
|
||||
|
||||
release_date = dateparser.parse(
|
||||
res_data['release_date'],
|
||||
settings={
|
||||
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)
|
||||
}
|
||||
)
|
||||
|
||||
other_info = {}
|
||||
if res_data['external_ids'].get('upc'):
|
||||
# bar code
|
||||
other_info['UPC'] = res_data['external_ids']['upc']
|
||||
|
||||
raw_img, ext = self.download_image(res_data['images'][0]['url'])
|
||||
|
||||
data = {
|
||||
'title': title,
|
||||
'artist': artist,
|
||||
'genre': genre,
|
||||
'track_list': track_list,
|
||||
'release_date': release_date,
|
||||
'duration': duration,
|
||||
'company': company,
|
||||
'brief': None,
|
||||
'other_info': other_info,
|
||||
'source_site': self.site_name,
|
||||
'source_url': effective_url,
|
||||
}
|
||||
|
||||
# set tracks_data, used for adding tracks
|
||||
self.track_urls = track_urls
|
||||
|
||||
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
|
||||
return data, raw_img
|
||||
|
||||
@classmethod
|
||||
def get_effective_url(cls, raw_url):
|
||||
code = cls.regex.findall(raw_url)
|
||||
if code:
|
||||
return f"https://open.spotify.com/album/{code[0]}"
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def save(cls, request_user):
|
||||
form = super().save(request_user)
|
||||
task = Thread(
|
||||
target=cls.add_tracks,
|
||||
args=(form.instance, request_user),
|
||||
daemon=True
|
||||
)
|
||||
task.start()
|
||||
return form
|
||||
|
||||
@classmethod
|
||||
def get_api_url(cls, url):
|
||||
return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0]
|
||||
|
||||
@classmethod
|
||||
def add_tracks(cls, album: Album, request_user):
|
||||
to_be_updated_tracks = []
|
||||
for track_url in cls.track_urls:
|
||||
track = cls.get_track_or_none(track_url)
|
||||
# seems lik if fire too many requests at the same time
|
||||
# spotify would limit access
|
||||
if track is None:
|
||||
task = Thread(
|
||||
target=cls.scrape_and_save_track,
|
||||
args=(track_url, album, request_user),
|
||||
daemon=True
|
||||
)
|
||||
task.start()
|
||||
task.join()
|
||||
else:
|
||||
to_be_updated_tracks.append(track)
|
||||
cls.bulk_update_track_album(to_be_updated_tracks, album, request_user)
|
||||
|
||||
@classmethod
|
||||
def get_track_or_none(cls, track_url: str):
|
||||
try:
|
||||
instance = Song.objects.get(source_url=track_url)
|
||||
return instance
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def scrape_and_save_track(cls, url: str, album: Album, request_user):
|
||||
data, img = SpotifyTrackScraper.scrape(url)
|
||||
SpotifyTrackScraper.raw_data['album'] = album
|
||||
SpotifyTrackScraper.save(request_user)
|
||||
|
||||
@classmethod
|
||||
def bulk_update_track_album(cls, tracks, album, request_user):
|
||||
for track in tracks:
|
||||
track.last_editor = request_user
|
||||
track.edited_time = timezone.now()
|
||||
track.album = album
|
||||
Song.objects.bulk_update(tracks, [
|
||||
'last_editor',
|
||||
'edited_time',
|
||||
'album'
|
||||
])
|
||||
|
||||
|
||||
def is_spotify_token_expired():
|
||||
global spotify_token_expire_time
|
||||
return True if spotify_token_expire_time <= time.time() else False
|
||||
|
||||
|
||||
def invoke_spotify_token():
|
||||
global spotify_token, spotify_token_expire_time
|
||||
r = requests.post(
|
||||
"https://accounts.spotify.com/api/token",
|
||||
data={
|
||||
"grant_type": "client_credentials"
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
|
||||
}
|
||||
)
|
||||
data = r.json()
|
||||
if r.status_code == 401:
|
||||
# token expired, try one more time
|
||||
# this maybe caused by external operations,
|
||||
# for example debugging using a http client
|
||||
r = requests.post(
|
||||
"https://accounts.spotify.com/api/token",
|
||||
data={
|
||||
"grant_type": "client_credentials"
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
|
||||
}
|
||||
)
|
||||
data = r.json()
|
||||
elif r.status_code != 200:
|
||||
raise Exception(f"Request to spotify API fails. Reason: {r.reason}")
|
||||
# minus 2 for execution time error
|
||||
spotify_token_expire_time = int(data['expires_in']) + time.time() - 2
|
||||
spotify_token = data['access_token']
|
||||
|
|
|
@ -374,6 +374,9 @@ input[type='search'],
|
|||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
input[type='date'],
|
||||
input[type='time'],
|
||||
input[type='color'],
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
|
@ -396,6 +399,9 @@ input[type='search']:focus,
|
|||
input[type='tel']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='url']:focus,
|
||||
input[type='date']:focus,
|
||||
input[type='time']:focus,
|
||||
input[type='color']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #00a1cc;
|
||||
|
@ -409,6 +415,9 @@ input[type='search']::-webkit-input-placeholder,
|
|||
input[type='tel']::-webkit-input-placeholder,
|
||||
input[type='text']::-webkit-input-placeholder,
|
||||
input[type='url']::-webkit-input-placeholder,
|
||||
input[type='date']::-webkit-input-placeholder,
|
||||
input[type='time']::-webkit-input-placeholder,
|
||||
input[type='color']::-webkit-input-placeholder,
|
||||
textarea::-webkit-input-placeholder,
|
||||
select::-webkit-input-placeholder {
|
||||
color: #ccc;
|
||||
|
@ -421,6 +430,9 @@ input[type='search']:-ms-input-placeholder,
|
|||
input[type='tel']:-ms-input-placeholder,
|
||||
input[type='text']:-ms-input-placeholder,
|
||||
input[type='url']:-ms-input-placeholder,
|
||||
input[type='date']:-ms-input-placeholder,
|
||||
input[type='time']:-ms-input-placeholder,
|
||||
input[type='color']:-ms-input-placeholder,
|
||||
textarea:-ms-input-placeholder,
|
||||
select:-ms-input-placeholder {
|
||||
color: #ccc;
|
||||
|
@ -433,6 +445,9 @@ input[type='search']::-ms-input-placeholder,
|
|||
input[type='tel']::-ms-input-placeholder,
|
||||
input[type='text']::-ms-input-placeholder,
|
||||
input[type='url']::-ms-input-placeholder,
|
||||
input[type='date']::-ms-input-placeholder,
|
||||
input[type='time']::-ms-input-placeholder,
|
||||
input[type='color']::-ms-input-placeholder,
|
||||
textarea::-ms-input-placeholder,
|
||||
select::-ms-input-placeholder {
|
||||
color: #ccc;
|
||||
|
@ -445,6 +460,9 @@ input[type='search']::placeholder,
|
|||
input[type='tel']::placeholder,
|
||||
input[type='text']::placeholder,
|
||||
input[type='url']::placeholder,
|
||||
input[type='date']::placeholder,
|
||||
input[type='time']::placeholder,
|
||||
input[type='color']::placeholder,
|
||||
textarea::placeholder,
|
||||
select::placeholder {
|
||||
color: #ccc;
|
||||
|
@ -1184,6 +1202,8 @@ select::placeholder {
|
|||
font-weight: lighter;
|
||||
letter-spacing: 0.1rem;
|
||||
word-break: keep-all;
|
||||
opacity: 0.8;
|
||||
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
@ -1194,8 +1214,16 @@ select::placeholder {
|
|||
}
|
||||
|
||||
.source-label.source-label__douban {
|
||||
border-color: #319840;
|
||||
color: #319840;
|
||||
border: none;
|
||||
color: white;
|
||||
background-color: #319840;
|
||||
}
|
||||
|
||||
.source-label.source-label__spotify {
|
||||
background-color: #1ed760;
|
||||
color: black;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.main-section-wrapper {
|
||||
|
@ -1308,6 +1336,8 @@ select::placeholder {
|
|||
object-fit: contain;
|
||||
float: left;
|
||||
max-width: 150px;
|
||||
-o-object-position: top;
|
||||
object-position: top;
|
||||
}
|
||||
|
||||
.entity-detail .entity-detail__info {
|
||||
|
@ -1377,7 +1407,7 @@ select::placeholder {
|
|||
}
|
||||
|
||||
.entity-desc .entity-desc__content--folded {
|
||||
max-height: 200px;
|
||||
max-height: 202px;
|
||||
}
|
||||
|
||||
.entity-desc .entity-desc__unfold-button {
|
||||
|
@ -1647,6 +1677,77 @@ select::placeholder {
|
|||
color: #00a1cc;
|
||||
}
|
||||
|
||||
.track-carousel {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.track-carousel::-webkit-scrollbar {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.track-carousel__content {
|
||||
display: -ms-grid;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
margin: auto;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.track-carousel__track {
|
||||
width: 10vw;
|
||||
height: 13vw;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.track-carousel__track img {
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.track-carousel__track-title {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.track-carousel__button {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-ms-flex-line-pack: center;
|
||||
align-content: center;
|
||||
background: white;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.track-carousel__button--prev {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
-webkit-transform: translate(50%, -50%);
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
.track-carousel__button--next {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.entity-list .entity-list__entity {
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -1720,6 +1821,10 @@ select::placeholder {
|
|||
.review-head .review-head__actions {
|
||||
float: unset;
|
||||
}
|
||||
.track-carousel__track {
|
||||
width: 32vw;
|
||||
height: 40vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
|
@ -1772,7 +1877,7 @@ select::placeholder {
|
|||
line-height: unset;
|
||||
height: unset;
|
||||
padding: 4px 15px;
|
||||
margin: 0 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.action-panel {
|
||||
|
|
2
common/static/css/boofilsic.min.css
vendored
2
common/static/css/boofilsic.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -8,6 +8,8 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
|
|||
if (placeholderValue == null) {
|
||||
placeholderValue = '';
|
||||
}
|
||||
// assign existing pairs to hidden input
|
||||
setHiddenInput(valueKeyWidget);
|
||||
|
||||
let newInputPair = $('<input type="text"' + 'placeholder=' + placeholderKey + '><input type="text"' + 'placeholder=' + placeholderValue + '>');
|
||||
valueKeyWidget.append(newInputPair.clone());
|
||||
|
@ -27,7 +29,7 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
|
|||
$(this).next().remove();
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
valueKeyWidget.on('input', ':nth-last-child(3)', function () {
|
||||
if (!$(this).val() && !$(this).prev().val() && valueKeyWidget.children("input").length > 2) {
|
||||
|
@ -37,12 +39,16 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
|
|||
});
|
||||
|
||||
valueKeyWidget.on('input', function () {
|
||||
let keys = $(this).children(":nth-child(odd)").map(function () {
|
||||
setHiddenInput(this);
|
||||
});
|
||||
|
||||
function setHiddenInput(elem) {
|
||||
let keys = $(elem).children(":nth-child(odd)").map(function () {
|
||||
if ($(this).val()) {
|
||||
return $(this).val();
|
||||
}
|
||||
}).get();
|
||||
let values = $(this).children(":nth-child(even)").map(function () {
|
||||
let values = $(elem).children(":nth-child(even)").map(function () {
|
||||
if ($(this).val()) {
|
||||
return $(this).val();
|
||||
}
|
||||
|
@ -55,7 +61,7 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
|
|||
finalValue.push(JSON.stringify(json))
|
||||
});
|
||||
hiddenInput.val(finalValue.toString());
|
||||
} else if(keys.length - values.length == 1) {
|
||||
} else if (keys.length - values.length == 1) {
|
||||
let finalValue = [];
|
||||
keys.forEach(function (key, i) {
|
||||
let json = new Object;
|
||||
|
@ -66,8 +72,10 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
|
|||
}
|
||||
finalValue.push(JSON.stringify(json))
|
||||
});
|
||||
hiddenInput.val(finalValue.toString());
|
||||
hiddenInput.val(finalValue.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ $aside-section-padding-mobile: 24px 25px 10px 25px
|
|||
line-height: unset;
|
||||
height: unset;
|
||||
padding: 4px 15px;
|
||||
margin: 0 5px;
|
||||
margin: 5px;
|
||||
|
||||
.action-panel
|
||||
margin-bottom: 20px
|
||||
|
|
|
@ -68,6 +68,9 @@ input[type='search'],
|
|||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
input[type='date'],
|
||||
input[type='time'],
|
||||
input[type='color'],
|
||||
textarea,
|
||||
select
|
||||
appearance: none // Removes awkward default styles on some inputs for iOS
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
// source label name should match the enum value in `common.models.SourceSiteEnum`
|
||||
|
||||
$douban-color: #319840
|
||||
$douban-color-primary: #319840
|
||||
$douban-color-secondary: white
|
||||
$in-site-color: $color-primary
|
||||
$spotify-color-primary: #1ed760
|
||||
$spotify-color-secondary: black
|
||||
|
||||
.source-label
|
||||
display: inline
|
||||
|
@ -17,6 +20,8 @@ $in-site-color: $color-primary
|
|||
font-weight: lighter
|
||||
letter-spacing: 0.1rem
|
||||
word-break: keep-all
|
||||
opacity: 0.8
|
||||
|
||||
|
||||
position: relative
|
||||
top: -1px
|
||||
|
@ -25,6 +30,12 @@ $in-site-color: $color-primary
|
|||
border-color: $in-site-color
|
||||
color: $in-site-color
|
||||
&.source-label__douban
|
||||
border-color: $douban-color
|
||||
color: $douban-color
|
||||
&.source-label__amazon
|
||||
border: none
|
||||
color: $douban-color-secondary
|
||||
background-color: $douban-color-primary
|
||||
&.source-label__amazon
|
||||
&.source-label__spotify
|
||||
background-color: $spotify-color-primary
|
||||
color: $spotify-color-secondary
|
||||
border: none
|
||||
font-weight: bold
|
|
@ -66,7 +66,9 @@ $sub-section-title-margin: 8px
|
|||
position: relative
|
||||
top: 0.52em
|
||||
&--full-length
|
||||
// display: block
|
||||
max-width: 100%
|
||||
// margin-bottom: 12px
|
||||
|
||||
& &__entity-brief
|
||||
margin-top: 8px
|
||||
|
@ -100,10 +102,11 @@ $sub-section-title-margin: 8px
|
|||
.entity-detail
|
||||
|
||||
& &__img
|
||||
height: 210px;
|
||||
object-fit: contain;
|
||||
float: left;
|
||||
max-width: 150px;
|
||||
height: 210px
|
||||
object-fit: contain
|
||||
float: left
|
||||
max-width: 150px
|
||||
object-position: top
|
||||
|
||||
& &__info
|
||||
float: left
|
||||
|
@ -158,7 +161,7 @@ $mark-review-padding-wider: 6px 0
|
|||
& &__content
|
||||
overflow: hidden
|
||||
&--folded
|
||||
max-height: 200px
|
||||
max-height: 202px
|
||||
|
||||
& &__unfold-button
|
||||
display: flex
|
||||
|
@ -373,6 +376,59 @@ $mark-review-padding-wider: 6px 0
|
|||
&:hover
|
||||
color: $color-primary
|
||||
|
||||
|
||||
.track-carousel
|
||||
position: relative
|
||||
overflow: auto
|
||||
scroll-behavior: smooth
|
||||
scrollbar-width: none
|
||||
margin-top: 5px
|
||||
// padding: 0
|
||||
|
||||
&::-webkit-scrollbar
|
||||
height: 0px
|
||||
&__content
|
||||
display: grid
|
||||
grid-gap: 16px
|
||||
margin: auto
|
||||
box-sizing: border-box
|
||||
grid-auto-flow: column
|
||||
// grid-template-columns: max-content
|
||||
&__track
|
||||
width: 10vw
|
||||
height: 13vw
|
||||
// grid-column: 1
|
||||
text-align: center
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
& img
|
||||
object-fit: contain
|
||||
&__track-title
|
||||
// word-break: keep-all
|
||||
// overflow-wrap: anywhere
|
||||
white-space: nowrap
|
||||
|
||||
&__button
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-content: center
|
||||
background: white
|
||||
border: none
|
||||
padding: 8px
|
||||
border-radius: 50%
|
||||
outline: 0
|
||||
cursor: pointer
|
||||
position: absolute
|
||||
&--prev
|
||||
top: 50%
|
||||
left: 0
|
||||
transform: translate(50%, -50%)
|
||||
&--next
|
||||
top: 50%
|
||||
right: 0
|
||||
transform: translate(-50%, -50%)
|
||||
|
||||
|
||||
// Small devices (landscape phones, 576px and up)
|
||||
@media (max-width: $small-devices)
|
||||
.entity-list
|
||||
|
@ -437,6 +493,11 @@ $mark-review-padding-wider: 6px 0
|
|||
float: unset
|
||||
& &__actions
|
||||
float: unset
|
||||
|
||||
.track-carousel
|
||||
&__track
|
||||
width: 32vw
|
||||
height: 40vw
|
||||
|
||||
|
||||
// Medium devices (tablets, 768px and up)
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="bookWish">
|
||||
<div class="entity-sort" id="bookDo">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '在读的书' %}
|
||||
</h5>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<h5 class="entity-sort__label">
|
||||
{% trans '想看的电影/剧集' %}
|
||||
</h5>
|
||||
{% if wish_movies_more %}
|
||||
{% if wish_music_more %}
|
||||
<a href="{% url 'users:movie_list' user.id 'wish' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
@ -139,7 +139,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="movieWish">
|
||||
<div class="entity-sort" id="movieDo">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '在看的电影/剧集' %}
|
||||
</h5>
|
||||
|
@ -191,6 +191,114 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="entity-sort" id="musicWish">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '想听的音乐' %}
|
||||
</h5>
|
||||
{% if wish_music_more %}
|
||||
<a href="{% url 'users:music_list' user.id 'wish' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for wish_music_mark in wish_music_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
|
||||
|
||||
{% if wish_music_mark.type == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' wish_music_mark.album.id %}">
|
||||
<img src="{{ wish_music_mark.album.cover.url }}"
|
||||
alt="{{wish_music_mark.album.title}}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{wish_music_mark.album.title}}">
|
||||
{{ wish_music_mark.album.title }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'music:retrieve_song' wish_music_mark.song.id %}">
|
||||
<img src="{{ wish_music_mark.song.cover.url }}" alt="{{wish_music_mark.song.title}}"
|
||||
class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{wish_music_mark.song.title}}">
|
||||
{{ wish_music_mark.song.title }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="musicDo">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '在听的音乐' %}
|
||||
</h5>
|
||||
{% if do_music_more %}
|
||||
<a href="{% url 'users:music_list' user.id 'do' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for do_music_mark in do_music_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
{% if do_music_mark.type == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' do_music_mark.album.id %}">
|
||||
<img src="{{ do_music_mark.album.cover.url }}"
|
||||
alt="{{do_music_mark.album.title}}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{do_music_mark.album.title}}">
|
||||
{{ do_music_mark.album.title }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'music:retrieve_song' do_music_mark.song.id %}">
|
||||
<img src="{{ do_music_mark.song.cover.url }}"
|
||||
alt="{{do_music_mark.song.title}}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{do_music_mark.song.title}}">
|
||||
{{ do_music_mark.song.title }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="musicCollect">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '听过的音乐' %}
|
||||
</h5>
|
||||
{% if collect_music_more %}
|
||||
<a href="{% url 'users:music_list' user.id 'collect' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for collect_music_mark in collect_music_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
{% if collect_music_mark.type == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' collect_music_mark.album.id %}">
|
||||
<img src="{{ collect_music_mark.album.cover.url }}"
|
||||
alt="{{collect_music_mark.album.title}}" class="entity-sort__entity-img">
|
||||
<span class="entity-sort__entity-name"
|
||||
title="{{collect_music_mark.album.title}}">{{ collect_music_mark.album.title }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'music:retrieve_song' collect_music_mark.song.id %}">
|
||||
<img src="{{ collect_music_mark.song.cover.url }}"
|
||||
alt="{{collect_music_mark.song.title}}" class="entity-sort__entity-img">
|
||||
<span class="entity-sort__entity-name"
|
||||
title="{{collect_music_mark.song.title}}">{{ collect_music_mark.song.title }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
<div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
|
||||
{% endif %}
|
||||
|
||||
<span class="entity-list__entity-info entity-list__entity-info--full-length">
|
||||
<span class="entity-list__entity-info ">
|
||||
|
||||
|
||||
{% if movie.director %}{% trans '导演' %}
|
||||
|
@ -187,7 +187,7 @@
|
|||
{% if movie.genre %}{% trans '类型' %}
|
||||
{% for genre in movie.get_genre_display %}
|
||||
{{ genre }}{% if not forloop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}/
|
||||
{% endif %}
|
||||
|
||||
</span>
|
||||
|
@ -196,13 +196,13 @@
|
|||
{% for actor in movie.actor %}
|
||||
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
|
||||
{% if forloop.counter <= 5 %}
|
||||
{% if not forloop.counter == 5 %} / {% endif %}
|
||||
{% if not forloop.counter == 5 and not forloop.last %} {% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<p class="entity-list__entity-brief">
|
||||
{{ movie.brief | truncate:170 }}
|
||||
{{ movie.brief }}
|
||||
</p>
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in movie.tag_list %}
|
||||
|
@ -219,6 +219,111 @@
|
|||
|
||||
</li>
|
||||
{% endwith %}
|
||||
|
||||
{% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %}
|
||||
|
||||
{% with music=item %}
|
||||
<li class="entity-list__entity">
|
||||
<div class="entity-list__entity-img-wrapper">
|
||||
|
||||
{% if item.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% elif item.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="entity-list__entity-text">
|
||||
<div class="entity-list__entity-title">
|
||||
|
||||
{% if item.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
|
||||
{% if request.GET.q %}
|
||||
{{ music.title | highlight:request.GET.q }}
|
||||
{% else %}
|
||||
{{ music.title }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% elif item.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
|
||||
{% if request.GET.q %}
|
||||
{{ music.title | highlight:request.GET.q }}
|
||||
{% else %}
|
||||
{{ music.title }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if not request.GET.c or request.GET.c != 'music' and request.GET.c != 'book' and request.GET.c != 'music' %}
|
||||
<span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
|
||||
{% endif %}
|
||||
<a href="{{ music.source_url }}">
|
||||
<span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if music.rating %}
|
||||
<div class="rating-star entity-list__rating-star" data-rating-score="{{ music.rating | floatformat:"0" }}"></div>
|
||||
<span class="entity-list__rating-score rating-score">{{ music.rating }}</span>
|
||||
{% else %}
|
||||
<div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
|
||||
{% endif %}
|
||||
|
||||
<span class="entity-list__entity-info ">
|
||||
{% if music.artist %}{% trans '艺术家' %}
|
||||
{% for artist in music.artist %}
|
||||
<span>{{ artist }}</span>
|
||||
{% if not forloop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if music.genre %}/ {% trans '流派' %}
|
||||
{{ music.genre }}
|
||||
{% endif %}
|
||||
|
||||
{% if music.release_date %}/ {% trans '发行日期' %}
|
||||
{{ music.release_date }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="entity-list__entity-info entity-list__entity-info--full-length">
|
||||
|
||||
</span>
|
||||
|
||||
{% if music.brief %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{{ music.brief }}
|
||||
</p>
|
||||
{% elif music.category_name|lower == 'album' %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '曲目:' %}{{ music.track_list }}
|
||||
</p>
|
||||
{% else %}
|
||||
<!-- song -->
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '所属专辑:' %}{{ music.album }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in music.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>
|
||||
|
||||
</li>
|
||||
{% endwith %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -278,6 +383,12 @@
|
|||
<a href="{% url 'movies:create' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加电影/剧集' %}</button>
|
||||
</a>
|
||||
|
||||
{% elif request.GET.c|lower == 'music' %}
|
||||
|
||||
<a href="{% url 'movies:create' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加音乐' %}</button>
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
<a href="{% url 'books:create' %}">
|
||||
|
@ -286,6 +397,12 @@
|
|||
<a href="{% url 'movies:create' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加电影/剧集' %}</button>
|
||||
</a>
|
||||
<a href="{% url 'music:create_album' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
|
||||
</a>
|
||||
<a href="{% url 'music:create_song' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加单曲' %}</button>
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -296,6 +413,12 @@
|
|||
<a href="{% url 'movies:create' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加电影/剧集' %}</button>
|
||||
</a>
|
||||
<a href="{% url 'music:create_album' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
|
||||
</a>
|
||||
<a href="{% url 'music:create_song' %}">
|
||||
<button class="add-entity-entries__button">{% trans '添加单曲' %}</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="add-entity-entries__entry">
|
||||
|
@ -319,6 +442,15 @@
|
|||
<button class="add-entity-entries__button">{% trans '从表瓣剽取数据' %}</button>
|
||||
</a>
|
||||
|
||||
{% elif request.GET.c|lower == 'music' %}
|
||||
|
||||
<div class="add-entity-entries__label">
|
||||
{% trans '或者(≖ ◡ ≖)✧' %}
|
||||
</div>
|
||||
<a href="{% url 'music:scrape_album' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
|
||||
<button class="add-entity-entries__button">{% trans '从表瓣剽取数据' %}</button>
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="add-entity-entries__label">
|
||||
|
@ -330,6 +462,9 @@
|
|||
<a href="{% url 'movies:scrape' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
|
||||
<button class="add-entity-entries__button">{% trans '电影/剧集' %}</button>
|
||||
</a>
|
||||
<a href="{% url 'music:scrape_album' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
|
||||
<button class="add-entity-entries__button">{% trans '专辑' %}</button>
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -344,7 +479,9 @@
|
|||
<a href="{% url 'movies:scrape' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
|
||||
<button class="add-entity-entries__button">{% trans '电影/剧集' %}</button>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'music:scrape_album' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
|
||||
<button class="add-entity-entries__button">{% trans '专辑' %}</button>
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic" id="githubLink">Github</a>
|
||||
<a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助项目</a>
|
||||
<a class="footer__link" target="_blank" href="/announcement/supported-sites/" id="supported-sites">支持的网站</a>
|
||||
<a class="footer__link" href="javascript:void();" id="version">Version 0.2.0</a>
|
||||
<a class="footer__link" href="javascript:void();" id="version">Version 0.3.0</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
|
@ -17,6 +17,7 @@
|
|||
<option value="all" {% if request.GET.c and request.GET.c != 'movie' and request.GET.c != 'book' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
|
||||
<option value="book" {% if request.GET.c and request.GET.c == 'book' %}selected{% endif %}>{% trans '书籍' %}</option>
|
||||
<option value="movie" {% if request.GET.c and request.GET.c == 'movie' %}selected{% endif %}>{% trans '电影' %}</option>
|
||||
<option value="music" {% if request.GET.c and request.GET.c == 'music' %}selected{% endif %}>{% trans '音乐' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="navbar__dropdown-btn">• • •</button>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<style>
|
||||
.widget-value-key-input input:nth-child(odd) {
|
||||
width: 49%;
|
||||
margin-right: 1%;
|
||||
}
|
||||
.widget-value-key-input input:nth-child(even) {
|
||||
width: 49%;
|
||||
margin-left: 1%;
|
||||
}
|
||||
</style>
|
||||
<div class="widget-value-key-input" name='{{ widget.name }}'{% include "django/forms/widgets/attrs.html" %}>
|
||||
{% if widget.value != None %}
|
||||
|
||||
{% for k, v in widget.keyvalue_pairs.items %}
|
||||
<input type="text" value="{{ k }}" ><input type="text" value="{{ v }}">
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="text" class="widget-value-key-input-data" hidden name="{{ widget.name }}">
|
||||
<script>
|
||||
keyValueInput(
|
||||
$(".widget-value-key-input[name='{{ widget.name }}']"),
|
||||
$(".widget-value-key-input-data[name='{{ widget.name }}']")
|
||||
);
|
||||
</script>
|
|
@ -3,11 +3,15 @@ from django.utils.safestring import mark_safe
|
|||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils.html import format_html
|
||||
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
@stringfilter
|
||||
def highlight(text, search):
|
||||
highlighted = text.replace(search, '<span class="highlight">{}</span>'.format(search))
|
||||
return mark_safe(highlighted)
|
||||
to_be_replaced_words = set(re.findall(search, text, flags=re.IGNORECASE))
|
||||
|
||||
for word in to_be_replaced_words:
|
||||
text = text.replace(word, f'<span class="highlight">{word}</span>')
|
||||
return mark_safe(text)
|
||||
|
|
179
common/views.py
179
common/views.py
|
@ -8,11 +8,11 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.core.paginator import Paginator
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db.models import Q, Count
|
||||
from django.http import HttpResponseBadRequest
|
||||
from books.models import Book
|
||||
from movies.models import Movie
|
||||
from music.models import Album, Song, AlbumMark, SongMark
|
||||
from users.models import Report, User
|
||||
from mastodon.decorators import mastodon_request_included
|
||||
from common.models import MarkStatusEnum
|
||||
|
@ -27,6 +27,8 @@ BOOKS_PER_SET = 5
|
|||
# how many movies have in each set at the home page
|
||||
MOVIES_PER_SET = 5
|
||||
|
||||
MUSIC_PER_SET = 5
|
||||
|
||||
# how many items are showed in one search result page
|
||||
ITEMS_PER_PAGE = 20
|
||||
|
||||
|
@ -42,6 +44,8 @@ logger = logging.getLogger(__name__)
|
|||
def home(request):
|
||||
if request.method == 'GET':
|
||||
|
||||
# really shitty code here
|
||||
|
||||
unread_announcements = Announcement.objects.filter(
|
||||
pk__gt=request.user.read_announcement_index).order_by('-pk')
|
||||
try:
|
||||
|
@ -76,6 +80,28 @@ def home(request):
|
|||
status=MarkStatusEnum.COLLECT).order_by("-edited_time")
|
||||
collect_movies_more = True if collect_movie_marks.count() > MOVIES_PER_SET else False
|
||||
|
||||
do_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \
|
||||
+ list(request.user.user_albummarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET])
|
||||
do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False
|
||||
do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
|
||||
|
||||
wish_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \
|
||||
+ list(request.user.user_albummarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET])
|
||||
wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False
|
||||
wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
|
||||
|
||||
collect_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \
|
||||
+ list(request.user.user_albummarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET])
|
||||
collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False
|
||||
collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
|
||||
|
||||
for mark in do_music_marks + wish_music_marks + collect_music_marks:
|
||||
# for template convenience
|
||||
if mark.__class__ == AlbumMark:
|
||||
mark.type = "album"
|
||||
else:
|
||||
mark.type = "song"
|
||||
|
||||
reports = Report.objects.order_by('-submitted_time').filter(is_read=False)
|
||||
# reports = Report.objects.latest('submitted_time').filter(is_read=False)
|
||||
|
||||
|
@ -95,6 +121,12 @@ def home(request):
|
|||
'do_movies_more': do_movies_more,
|
||||
'wish_movies_more': wish_movies_more,
|
||||
'collect_movies_more': collect_movies_more,
|
||||
'do_music_marks': do_music_marks,
|
||||
'wish_music_marks': wish_music_marks,
|
||||
'collect_music_marks': collect_music_marks,
|
||||
'do_music_more': do_music_more,
|
||||
'wish_music_more': wish_music_more,
|
||||
'collect_music_more': collect_music_more,
|
||||
'reports': reports,
|
||||
'unread_announcements': unread_announcements,
|
||||
}
|
||||
|
@ -122,22 +154,30 @@ def search(request):
|
|||
except ValidationError as e:
|
||||
pass
|
||||
|
||||
# category, book/movie/record etc
|
||||
# category, book/movie/music etc
|
||||
category = request.GET.get("c", default='').strip().lower()
|
||||
# keywords, seperated by blank space
|
||||
keywords = request.GET.get("q", default='').strip().split()
|
||||
# tag, when tag is provided there should be no keywords , for now
|
||||
tag = request.GET.get("tag", default='')
|
||||
|
||||
def book_param_handler():
|
||||
q = Q()
|
||||
query_args = []
|
||||
# white space string, empty query
|
||||
if not (keywords or tag):
|
||||
return []
|
||||
|
||||
def book_param_handler(**kwargs):
|
||||
# keywords
|
||||
keywords = request.GET.get("q", default='').strip()
|
||||
keywords = kwargs.get('keywords')
|
||||
# tag
|
||||
tag = kwargs.get('tag')
|
||||
|
||||
for keyword in [keywords]:
|
||||
query_args = []
|
||||
q = Q()
|
||||
|
||||
for keyword in keywords:
|
||||
q = q | Q(title__icontains=keyword)
|
||||
q = q | Q(subtitle__icontains=keyword)
|
||||
q = q | Q(orig_title__icontains=keyword)
|
||||
|
||||
# tag
|
||||
tag = request.GET.get("tag", default='')
|
||||
if tag:
|
||||
q = q & Q(book_tags__content__iexact=tag)
|
||||
|
||||
|
@ -158,6 +198,8 @@ def search(request):
|
|||
elif tag:
|
||||
# search by single tag
|
||||
book.similarity = 0 if book.rating_number is None else book.rating_number
|
||||
else:
|
||||
book.similarity = 0
|
||||
return book.similarity
|
||||
if len(queryset) > 0:
|
||||
ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
|
||||
|
@ -165,19 +207,19 @@ def search(request):
|
|||
ordered_queryset = list(queryset)
|
||||
return ordered_queryset
|
||||
|
||||
def movie_param_handler():
|
||||
q = Q()
|
||||
query_args = []
|
||||
def movie_param_handler(**kwargs):
|
||||
# keywords
|
||||
keywords = request.GET.get("q", default='').strip()
|
||||
keywords = kwargs.get('keywords')
|
||||
# tag
|
||||
tag = kwargs.get('tag')
|
||||
|
||||
for keyword in [keywords]:
|
||||
query_args = []
|
||||
q = Q()
|
||||
|
||||
for keyword in keywords:
|
||||
q = q | Q(title__icontains=keyword)
|
||||
q = q | Q(other_title__icontains=keyword)
|
||||
q = q | Q(orig_title__icontains=keyword)
|
||||
|
||||
# tag
|
||||
tag = request.GET.get("tag", default='')
|
||||
if tag:
|
||||
q = q & Q(movie_tags__content__iexact=tag)
|
||||
|
||||
|
@ -197,6 +239,8 @@ def search(request):
|
|||
elif tag:
|
||||
# search by single tag
|
||||
movie.similarity = 0 if movie.rating_number is None else movie.rating_number
|
||||
else:
|
||||
movie.similarity = 0
|
||||
return movie.similarity
|
||||
if len(queryset) > 0:
|
||||
ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
|
||||
|
@ -204,11 +248,71 @@ def search(request):
|
|||
ordered_queryset = list(queryset)
|
||||
return ordered_queryset
|
||||
|
||||
def all_param_handler():
|
||||
book_queryset = book_param_handler()
|
||||
movie_queryset = movie_param_handler()
|
||||
def music_param_handler(**kwargs):
|
||||
# keywords
|
||||
keywords = kwargs.get('keywords')
|
||||
# tag
|
||||
tag = kwargs.get('tag')
|
||||
|
||||
query_args = []
|
||||
q = Q()
|
||||
|
||||
# search albums
|
||||
for keyword in keywords:
|
||||
q = q | Q(title__icontains=keyword)
|
||||
q = q | Q(artist__icontains=keyword)
|
||||
if tag:
|
||||
q = q & Q(album_tags__content__iexact=tag)
|
||||
|
||||
query_args.append(q)
|
||||
album_queryset = Album.objects.filter(*query_args).distinct()
|
||||
|
||||
# extra query args for songs
|
||||
q = Q()
|
||||
for keyword in keywords:
|
||||
q = q | Q(album__title__icontains=keyword)
|
||||
q = q | Q(title__icontains=keyword)
|
||||
q = q | Q(artist__icontains=keyword)
|
||||
if tag:
|
||||
q = q & Q(song_tags__content__iexact=tag)
|
||||
query_args.clear()
|
||||
query_args.append(q)
|
||||
song_queryset = Song.objects.filter(*query_args).distinct()
|
||||
queryset = list(album_queryset) + list(song_queryset)
|
||||
|
||||
def calculate_similarity(music):
|
||||
if keywords:
|
||||
# search by name
|
||||
similarity, n = 0, 0
|
||||
artist_dump = ' '.join(music.artist)
|
||||
for keyword in keywords:
|
||||
if music.__class__ == Album:
|
||||
similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
|
||||
+ 1/2 * SequenceMatcher(None, keyword, artist_dump).quick_ratio()
|
||||
elif music.__class__ == Song:
|
||||
similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
|
||||
+ 1/6 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() \
|
||||
+ 1/6 * SequenceMatcher(None, keyword, music.album.title).quick_ratio()
|
||||
n += 1
|
||||
music.similarity = similarity / n
|
||||
elif tag:
|
||||
# search by single tag
|
||||
music.similarity = 0 if music.rating_number is None else music.rating_number
|
||||
else:
|
||||
music.similarity = 0
|
||||
return music.similarity
|
||||
if len(queryset) > 0:
|
||||
ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
|
||||
else:
|
||||
ordered_queryset = list(queryset)
|
||||
return ordered_queryset
|
||||
|
||||
def all_param_handler(**kwargs):
|
||||
book_queryset = book_param_handler(**kwargs)
|
||||
movie_queryset = movie_param_handler(**kwargs)
|
||||
music_queryset = music_param_handler(**kwargs)
|
||||
ordered_queryset = sorted(
|
||||
book_queryset + movie_queryset,
|
||||
book_queryset + movie_queryset + music_queryset,
|
||||
key=operator.attrgetter('similarity'),
|
||||
reverse=True
|
||||
)
|
||||
|
@ -217,14 +321,21 @@ def search(request):
|
|||
param_handler = {
|
||||
'book': book_param_handler,
|
||||
'movie': movie_param_handler,
|
||||
'music': music_param_handler,
|
||||
'all': all_param_handler,
|
||||
'': all_param_handler
|
||||
}
|
||||
|
||||
try:
|
||||
queryset = param_handler[category]()
|
||||
queryset = param_handler[category](
|
||||
keywords=keywords,
|
||||
tag=tag
|
||||
)
|
||||
except KeyError as e:
|
||||
queryset = param_handler['all']()
|
||||
queryset = param_handler['all'](
|
||||
keywords=keywords,
|
||||
tag=tag
|
||||
)
|
||||
paginator = Paginator(queryset, ITEMS_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
items = paginator.get_page(page_number)
|
||||
|
@ -279,17 +390,11 @@ def jump_or_scrape(request, url):
|
|||
except ObjectDoesNotExist:
|
||||
# scrape if not exists
|
||||
try:
|
||||
scraped_entity, raw_cover = scraper.scrape(url)
|
||||
except:
|
||||
scraper.scrape(url)
|
||||
form = scraper.save(request_user=request.user)
|
||||
except Exception as e:
|
||||
logger.error(f"Scrape Failed URL: {url}")
|
||||
logger.error("Expections during saving scraped data:", exc_info=e)
|
||||
return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
|
||||
scraped_cover = {
|
||||
'cover': SimpleUploadedFile('temp.jpg', raw_cover)}
|
||||
form = scraper.form_class(scraped_entity, scraped_cover)
|
||||
if form.is_valid():
|
||||
form.instance.last_editor = request.user
|
||||
form.save()
|
||||
return redirect(form.instance)
|
||||
else:
|
||||
msg = _("爬取数据失败😫")
|
||||
logger.error(str(form.errors))
|
||||
return render(request, 'common/error.html', {'msg': msg})
|
||||
return redirect(form.instance)
|
||||
|
||||
|
|
|
@ -16,11 +16,6 @@ def MovieMarkStatusTranslator(status):
|
|||
|
||||
|
||||
class MovieForm(forms.ModelForm):
|
||||
# pub_year = forms.IntegerField(
|
||||
# required=False, max_value=9999, min_value=0, label=_("出版年份"))
|
||||
# pub_month = forms.IntegerField(
|
||||
# required=False, max_value=12, min_value=1, label=_("出版月份"))
|
||||
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
genre = forms.MultipleChoiceField(
|
||||
required=False,
|
||||
|
@ -103,52 +98,18 @@ class MovieForm(forms.ModelForm):
|
|||
'is_series': forms.CheckboxInput(attrs={'style': 'width: auto; position: relative; top: 2px'})
|
||||
}
|
||||
|
||||
# def clean_isbn(self):
|
||||
# isbn = self.cleaned_data.get('isbn')
|
||||
# if isbn:
|
||||
# isbn = isbn.strip()
|
||||
# return isbn
|
||||
|
||||
class MovieMarkForm(MarkForm):
|
||||
|
||||
class MovieMarkForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
STATUS_CHOICES = [(v, MovieMarkStatusTranslator(v))
|
||||
for v in MarkStatusEnum.values]
|
||||
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
share_to_mastodon = forms.BooleanField(
|
||||
label=_("分享到长毛象"), initial=True, required=False)
|
||||
rating = forms.IntegerField(
|
||||
validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
|
||||
status = forms.ChoiceField(
|
||||
label=_(""),
|
||||
widget=forms.RadioSelect(),
|
||||
choices=STATUS_CHOICES
|
||||
)
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
tags = TagField(
|
||||
required=False,
|
||||
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
|
||||
label=_("标签")
|
||||
)
|
||||
text = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"placeholder": _("最多只能写360字哦~"),
|
||||
"maxlength": 360
|
||||
}
|
||||
),
|
||||
|
||||
label=_("短评"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MovieMark
|
||||
|
@ -168,19 +129,7 @@ class MovieMarkForm(forms.ModelForm):
|
|||
}
|
||||
|
||||
|
||||
class MovieReviewForm(forms.ModelForm):
|
||||
IS_PRIVATE_CHOICES = [
|
||||
(True, _("仅关注者")),
|
||||
(False, _("公开")),
|
||||
]
|
||||
share_to_mastodon = forms.BooleanField(
|
||||
label=_("分享到长毛象"), initial=True, required=False)
|
||||
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
is_private = RadioBooleanField(
|
||||
label=_("可见性"),
|
||||
initial=True,
|
||||
choices=IS_PRIVATE_CHOICES
|
||||
)
|
||||
class MovieReviewForm(ReviewForm):
|
||||
|
||||
class Meta:
|
||||
model = MovieReview
|
||||
|
|
|
@ -54,6 +54,7 @@ class MovieGenreEnum(models.TextChoices):
|
|||
REALITY_TV = 'Reality-TV', _('真人秀')
|
||||
FAMILY = 'Family', _('家庭')
|
||||
TALK_SHOW = 'Talk-Show', _('脱口秀')
|
||||
OTHER = 'Other', _('其他')
|
||||
|
||||
|
||||
MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum)
|
||||
|
@ -76,7 +77,7 @@ class Movie(Entity):
|
|||
default=list,
|
||||
)
|
||||
imdb_code = models.CharField(
|
||||
blank=True, max_length=10, null=True, db_index=True)
|
||||
blank=True, max_length=10, null=False, db_index=True, default='')
|
||||
director = postgres.ArrayField(
|
||||
models.CharField(_("director"), blank=True,
|
||||
default='', max_length=100),
|
||||
|
|
|
@ -59,9 +59,12 @@
|
|||
{% endif %}
|
||||
|
||||
{% if movie.last_editor %}
|
||||
<a href="{% url 'users:home' movie.last_editor.id %}">
|
||||
<div>{% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}</div>
|
||||
</a>
|
||||
<div>
|
||||
{% trans '最近编辑者:' %}
|
||||
<a href="{% url 'users:home' movie.last_editor.id %}">
|
||||
<span>{{ movie.last_editor | default:"" }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>{% trans '上次编辑时间:' %}{{ movie.edited_time }}</div>
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
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>
|
||||
|
|
|
@ -204,7 +204,7 @@
|
|||
<a href="{% url 'movies:update' movie.id %}">{% trans '编辑这部电影' %}</a>
|
||||
{% endif %}
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'movies:delete' movie.id %}"> / {% trans '删除' %}</a>
|
||||
/<a href="{% url 'movies:delete' movie.id %}"> {% trans '删除' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -225,30 +225,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if movie.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
{% if movie.brief %}
|
||||
|
||||
<p class="entity-desc__content">{{ movie.brief | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div>{% trans '暂无简介' %}</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if movie.contents %}
|
||||
<div class="entity-desc" id="contents">
|
||||
<h5 class="entity-desc__title">{% trans '目录' %}</h5>
|
||||
<p class="entity-desc__content">{{ movie.contents | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="entity-marks">
|
||||
|
||||
|
@ -270,7 +258,7 @@
|
|||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
|
@ -299,7 +287,7 @@
|
|||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
|
@ -327,7 +315,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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>
|
||||
|
@ -375,7 +363,7 @@
|
|||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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">
|
||||
|
|
|
@ -47,8 +47,7 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
|
|
@ -39,8 +39,7 @@
|
|||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.db import IntegrityError, transaction
|
|||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from mastodon import mastodon_request_included
|
||||
from mastodon.api import check_visibility, post_toot, TootVisibilityEnum
|
||||
from mastodon.utils import rating_to_emoji
|
||||
|
@ -17,7 +16,6 @@ from common.views import PAGE_LINK_NUMBER, jump_or_scrape
|
|||
from common.models import SourceSiteEnum
|
||||
from .models import *
|
||||
from .forms import *
|
||||
from .forms import MovieMarkStatusTranslator
|
||||
from boofilsic.settings import MASTODON_TAGS
|
||||
|
||||
|
||||
|
@ -225,7 +223,7 @@ def retrieve(request, id):
|
|||
}
|
||||
)
|
||||
else:
|
||||
logger.warning('non-GET method at /movie/<id>')
|
||||
logger.warning('non-GET method at /movies/<id>')
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
|
|
0
music/__init__.py
Normal file
0
music/__init__.py
Normal file
11
music/admin.py
Normal file
11
music/admin.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.contrib import admin
|
||||
from .models import *
|
||||
|
||||
admin.site.register(Song)
|
||||
admin.site.register(SongMark)
|
||||
admin.site.register(SongReview)
|
||||
admin.site.register(SongTag)
|
||||
admin.site.register(Album)
|
||||
admin.site.register(AlbumMark)
|
||||
admin.site.register(AlbumReview)
|
||||
admin.site.register(AlbumTag)
|
5
music/apps.py
Normal file
5
music/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MusicConfig(AppConfig):
|
||||
name = 'music'
|
180
music/forms.py
Normal file
180
music/forms.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
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):
|
||||
trans_dict = {
|
||||
MarkStatusEnum.DO.value: _("在听"),
|
||||
MarkStatusEnum.WISH.value: _("想听"),
|
||||
MarkStatusEnum.COLLECT.value: _("听过")
|
||||
}
|
||||
return trans_dict[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',
|
||||
'is_private',
|
||||
]
|
||||
labels = {
|
||||
'rating': _("评分"),
|
||||
}
|
||||
widgets = {
|
||||
'song': forms.TextInput(attrs={"hidden": ""}),
|
||||
}
|
||||
|
||||
|
||||
class SongReviewForm(ReviewForm):
|
||||
|
||||
class Meta:
|
||||
model = SongReview
|
||||
fields = [
|
||||
'id',
|
||||
'song',
|
||||
'title',
|
||||
'content',
|
||||
'is_private'
|
||||
]
|
||||
labels = {
|
||||
'song': "",
|
||||
'title': _("标题"),
|
||||
'content': _("正文"),
|
||||
'share_to_mastodon': _("分享到长毛象")
|
||||
}
|
||||
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',
|
||||
'is_private',
|
||||
]
|
||||
labels = {
|
||||
'rating': _("评分"),
|
||||
}
|
||||
widgets = {
|
||||
'album': forms.TextInput(attrs={"hidden": ""}),
|
||||
}
|
||||
|
||||
|
||||
class AlbumReviewForm(ReviewForm):
|
||||
|
||||
class Meta:
|
||||
model = AlbumReview
|
||||
fields = [
|
||||
'id',
|
||||
'album',
|
||||
'title',
|
||||
'content',
|
||||
'is_private'
|
||||
]
|
||||
labels = {
|
||||
'album': "",
|
||||
'title': _("标题"),
|
||||
'content': _("正文"),
|
||||
'share_to_mastodon': _("分享到长毛象")
|
||||
}
|
||||
widgets = {
|
||||
'album': forms.TextInput(attrs={"hidden": ""}),
|
||||
}
|
181
music/models.py
Normal file
181
music/models.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
import uuid
|
||||
import django.contrib.postgres.fields as postgres
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.shortcuts import reverse
|
||||
from common.models import Entity, Mark, Review, Tag
|
||||
from common.utils import ChoicesDictGenerator
|
||||
from boofilsic.settings import SONG_MEDIA_PATH_ROOT, DEFAULT_SONG_IMAGE, ALBUM_MEDIA_PATH_ROOT, DEFAULT_ALBUM_IMAGE
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def song_cover_path(instance, filename):
|
||||
ext = filename.split('.')[-1]
|
||||
filename = "%s.%s" % (uuid.uuid4(), ext)
|
||||
root = ''
|
||||
if SONG_MEDIA_PATH_ROOT.endswith('/'):
|
||||
root = SONG_MEDIA_PATH_ROOT
|
||||
else:
|
||||
root = SONG_MEDIA_PATH_ROOT + '/'
|
||||
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||
|
||||
|
||||
def album_cover_path(instance, filename):
|
||||
ext = filename.split('.')[-1]
|
||||
filename = "%s.%s" % (uuid.uuid4(), ext)
|
||||
root = ''
|
||||
if ALBUM_MEDIA_PATH_ROOT.endswith('/'):
|
||||
root = ALBUM_MEDIA_PATH_ROOT
|
||||
else:
|
||||
root = ALBUM_MEDIA_PATH_ROOT + '/'
|
||||
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||
|
||||
|
||||
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=DEFAULT_ALBUM_IMAGE, blank=True)
|
||||
duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
|
||||
artist = postgres.ArrayField(
|
||||
models.CharField(_("artist"), blank=True,
|
||||
default='', max_length=100),
|
||||
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="")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("music:retrieve_album", args=[self.id])
|
||||
|
||||
def get_tags_manager(self):
|
||||
return self.album_tags
|
||||
|
||||
@property
|
||||
def verbose_category_name(self):
|
||||
return _("专辑")
|
||||
|
||||
|
||||
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=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=_("所属专辑"))
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("music:retrieve_song", args=[self.id])
|
||||
|
||||
def get_tags_manager(self):
|
||||
return self.song_tags
|
||||
|
||||
@property
|
||||
def verbose_category_name(self):
|
||||
return _("单曲")
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
|
||||
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")
|
||||
]
|
||||
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
|
||||
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")
|
||||
]
|
453
music/templates/music/album_detail.html
Normal file
453
music/templates/music/album_detail.html
Normal file
|
@ -0,0 +1,453 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load strip_scheme %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title" content="NiceDB音乐 - {{ 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="NiceDB">
|
||||
<meta property="og:description"content="{{ album.brief }}">
|
||||
|
||||
<title>{% trans 'NiceDB - 音乐详情' %} | {{ album.title }}</title>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/detail.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content">
|
||||
<div class="grid">
|
||||
<div class="grid__main" id="main">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="entity-detail">
|
||||
|
||||
<img src="{{ album.cover.url }}" class="entity-detail__img" alt="{{ album.title }}">
|
||||
|
||||
<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 %}
|
||||
<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>
|
||||
{% 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").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").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}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if album.last_editor %}
|
||||
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.id %}">{{ 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.url }}" alt="{{ song }}" class="track-carousel__track-image">
|
||||
<span class="track-carousel__track-title">
|
||||
{{ song }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- <button 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>
|
||||
</button>
|
||||
<button 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>
|
||||
</button> -->
|
||||
</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 %}
|
||||
{% if mark_list %}
|
||||
<ul class="entity-marks__mark-list">
|
||||
{% for others_mark in mark_list %}
|
||||
<li class="entity-marks__mark">
|
||||
<a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
|
||||
<span>{{ others_mark.get_status_display }}</span>
|
||||
{% if others_mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ others_mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>{% trans '暂无标记' %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="entity-reviews">
|
||||
<h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
|
||||
|
||||
{% if review_list_more %}
|
||||
<a href="{% url '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.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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.is_private %}
|
||||
<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.edited_time }}</div>
|
||||
|
||||
{% if mark.text %}
|
||||
<p class="mark-panel__text">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
<div class="tag-collection">
|
||||
|
||||
{% for tag in mark_tags %}
|
||||
<span class="tag-collection__tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="action-panel" id="addMarkPanel">
|
||||
<div class="action-panel__label">{% trans '标记这部作品' %}</div>
|
||||
<div class="action-panel__button-group">
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '听过' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="aside-section-wrapper">
|
||||
{% if review %}
|
||||
<div class="review-panel">
|
||||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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>
|
||||
|
||||
</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.is_private.label }}:</span>
|
||||
{{ mark_form.is_private }}
|
||||
</div>
|
||||
<div class="mark-modal__share-checkbox">
|
||||
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mark-modal__confirm-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '提交' %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal modal">
|
||||
<div class="confirm-modal__head">
|
||||
<span class="confirm-modal__title">{% trans '确定要删除你的标记吗?' %}</span>
|
||||
<span class="confirm-modal__close-button modal-close">
|
||||
<span class="icon-cross">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<polygon
|
||||
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
|
||||
</polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="confirm-modal__body">
|
||||
<div class="confirm-modal__confirm-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '确认' %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-mask"></div>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
169
music/templates/music/album_mark_list.html
Normal file
169
music/templates/music/album_mark_list.html
Normal file
|
@ -0,0 +1,169 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ album.title }}{% trans '的标记' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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>
|
||||
<ul class="entity-marks__mark-list">
|
||||
|
||||
{% for mark in marks %}
|
||||
|
||||
<li class="entity-marks__mark entity-marks__mark--wider">
|
||||
<a href="{% url 'users:home' mark.owner.id %}"
|
||||
class="entity-marks__owner-link">{{ mark.owner.username }}</a>
|
||||
<span>{{ mark.get_status_display }}</span>
|
||||
{% if mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
|
||||
{% if mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
{% empty %}
|
||||
<div>
|
||||
{% trans '无结果' %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
|
||||
{% if marks.pagination.has_prev %}
|
||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</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.url }}"
|
||||
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").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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
156
music/templates/music/album_review_detail.html
Normal file
156
music/templates/music/album_review_detail.html
Normal file
|
@ -0,0 +1,156 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:article:author" content="{{ review.owner.username }}">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
|
||||
<title>{% trans 'NiceDB - 评论详情' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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="review-head">
|
||||
<h5 class="review-head__title">
|
||||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<div class="review-head__body">
|
||||
<div class="review-head__info">
|
||||
|
||||
<a href="{% url 'users:home' review.owner.id %}"
|
||||
class="review-head__owner-link">{{ review.owner.username }}</a>
|
||||
|
||||
{% if mark %}
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="review-head__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<span class="review-head__time">{{ review.edited_time }}</span>
|
||||
|
||||
</div>
|
||||
<div class="review-head__actions">
|
||||
{% if request.user == review.owner %}
|
||||
<a class="review-head__action-link"
|
||||
href="{% url '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 }}
|
||||
</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.url }}"
|
||||
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").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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
$(".markdownx textarea").hide();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
166
music/templates/music/album_review_list.html
Normal file
166
music/templates/music/album_review_list.html
Normal file
|
@ -0,0 +1,166 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ album.title }}{% trans '的评论' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.id %}"
|
||||
class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<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.url }}"
|
||||
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").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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
94
music/templates/music/create_update_album.html
Normal file
94
music/templates/music/create_update_album.html
Normal file
|
@ -0,0 +1,94 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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">
|
||||
<a href="{% url 'music:scrape_album' %}"
|
||||
class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
|
||||
<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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<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>
|
131
music/templates/music/create_update_album_review.html
Normal file
131
music/templates/music/create_update_album_review.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.url }}" alt="" class="item-image float-left">
|
||||
</a>
|
||||
</div>
|
||||
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
|
||||
|
||||
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
|
||||
{{ album.title }}
|
||||
</a>
|
||||
<a href="{{ album.source_url }}"><a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a></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").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.is_private.label }}{{ form.is_private }}
|
||||
</div>
|
||||
<div class="review-form__share-checkbox">
|
||||
{{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<input class="button float-right" type="submit" value="{% trans '提交' %}">
|
||||
</div>
|
||||
{{ form.media }}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
97
music/templates/music/create_update_song.html
Normal file
97
music/templates/music/create_update_song.html
Normal file
|
@ -0,0 +1,97 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<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>
|
135
music/templates/music/create_update_song_review.html
Normal file
135
music/templates/music/create_update_song_review.html
Normal file
|
@ -0,0 +1,135 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.url }}" alt="" class="item-image float-left">
|
||||
</a>
|
||||
</div>
|
||||
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
|
||||
|
||||
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
|
||||
{{ song.title }}
|
||||
</a>
|
||||
<a href="{{ song.source_url }}"><a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a></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").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.is_private.label }}{{ form.is_private }}
|
||||
</div>
|
||||
<div class="review-form__share-checkbox">
|
||||
{{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<input class="button float-right" type="submit" value="{% trans '提交' %}">
|
||||
</div>
|
||||
{{ form.media }}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
104
music/templates/music/delete_album.html
Normal file
104
music/templates/music/delete_album.html
Normal file
|
@ -0,0 +1,104 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - 删除音乐' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.url }}" 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.id %}">
|
||||
<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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
107
music/templates/music/delete_album_review.html
Normal file
107
music/templates/music/delete_album_review.html
Normal file
|
@ -0,0 +1,107 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - 删除评论' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.is_private %}
|
||||
<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.id %}"
|
||||
class="review-head__owner-link">{{ review.owner.username }}</a>
|
||||
|
||||
{% if mark %}
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="review-head__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<span class="review-head__time">{{ review.edited_time }}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="rawContent" class="delete-preview">
|
||||
{{ form.content }}
|
||||
</div>
|
||||
{{ form.media }}
|
||||
|
||||
<div class="dividing-line"></div>
|
||||
|
||||
<div class="clearfix">
|
||||
<form action="{% url '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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
$(".markdownx textarea").hide();
|
||||
$(".markdownx .markdownx-preview").show();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
104
music/templates/music/delete_song.html
Normal file
104
music/templates/music/delete_song.html
Normal file
|
@ -0,0 +1,104 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - 删除音乐' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.url }}" 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.id %}">
|
||||
<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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
107
music/templates/music/delete_song_review.html
Normal file
107
music/templates/music/delete_song_review.html
Normal file
|
@ -0,0 +1,107 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - 删除评论' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.is_private %}
|
||||
<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.id %}"
|
||||
class="review-head__owner-link">{{ review.owner.username }}</a>
|
||||
|
||||
{% if mark %}
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="review-head__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<span class="review-head__time">{{ review.edited_time }}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="rawContent" class="delete-preview">
|
||||
{{ form.content }}
|
||||
</div>
|
||||
{{ form.media }}
|
||||
|
||||
<div class="dividing-line"></div>
|
||||
|
||||
<div class="clearfix">
|
||||
<form action="{% url '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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
$(".markdownx textarea").hide();
|
||||
$(".markdownx .markdownx-preview").show();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
109
music/templates/music/scrape_album.html
Normal file
109
music/templates/music/scrape_album.html
Normal file
|
@ -0,0 +1,109 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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/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.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 '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>
|
109
music/templates/music/scrape_song.html
Normal file
109
music/templates/music/scrape_song.html
Normal file
|
@ -0,0 +1,109 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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>
|
400
music/templates/music/song_detail.html
Normal file
400
music/templates/music/song_detail.html
Normal file
|
@ -0,0 +1,400 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load strip_scheme %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title" content="NiceDB音乐 - {{ 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="NiceDB">
|
||||
<meta property="og:description"content="{{ song.brief }}">
|
||||
|
||||
<title>{% trans 'NiceDB - 音乐详情' %} | {{ song.title }}</title>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/detail.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content">
|
||||
<div class="grid">
|
||||
<div class="grid__main" id="main">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="entity-detail">
|
||||
|
||||
<img src="{{ song.cover.url }}" class="entity-detail__img" alt="{{ song.title }}">
|
||||
|
||||
<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 %}
|
||||
<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>
|
||||
{% 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").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}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if song.last_editor %}
|
||||
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.id %}">{{ 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 %}
|
||||
{% if mark_list %}
|
||||
<ul class="entity-marks__mark-list">
|
||||
{% for others_mark in mark_list %}
|
||||
<li class="entity-marks__mark">
|
||||
<a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
|
||||
<span>{{ others_mark.get_status_display }}</span>
|
||||
{% if others_mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ others_mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>{% trans '暂无标记' %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="entity-reviews">
|
||||
<h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
|
||||
|
||||
{% if review_list_more %}
|
||||
<a href="{% url '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.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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.is_private %}
|
||||
<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.edited_time }}</div>
|
||||
|
||||
{% if mark.text %}
|
||||
<p class="mark-panel__text">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
<div class="tag-collection">
|
||||
|
||||
{% for tag in mark_tags %}
|
||||
<span class="tag-collection__tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="action-panel" id="addMarkPanel">
|
||||
<div class="action-panel__label">{% trans '标记这部作品' %}</div>
|
||||
<div class="action-panel__button-group">
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '听过' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="aside-section-wrapper">
|
||||
{% if review %}
|
||||
<div class="review-panel">
|
||||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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>
|
||||
|
||||
</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.is_private.label }}:</span>
|
||||
{{ mark_form.is_private }}
|
||||
</div>
|
||||
<div class="mark-modal__share-checkbox">
|
||||
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mark-modal__confirm-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '提交' %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal modal">
|
||||
<div class="confirm-modal__head">
|
||||
<span class="confirm-modal__title">{% trans '确定要删除你的标记吗?' %}</span>
|
||||
<span class="confirm-modal__close-button modal-close">
|
||||
<span class="icon-cross">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<polygon
|
||||
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
|
||||
</polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="confirm-modal__body">
|
||||
<div class="confirm-modal__confirm-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '确认' %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-mask"></div>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
175
music/templates/music/song_mark_list.html
Normal file
175
music/templates/music/song_mark_list.html
Normal file
|
@ -0,0 +1,175 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的标记' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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>
|
||||
<ul class="entity-marks__mark-list">
|
||||
|
||||
{% for mark in marks %}
|
||||
|
||||
<li class="entity-marks__mark entity-marks__mark--wider">
|
||||
<a href="{% url 'users:home' mark.owner.id %}"
|
||||
class="entity-marks__owner-link">{{ mark.owner.username }}</a>
|
||||
<span>{{ mark.get_status_display }}</span>
|
||||
{% if mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
|
||||
{% if mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
{% empty %}
|
||||
<div>
|
||||
{% trans '无结果' %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
|
||||
{% if marks.pagination.has_prev %}
|
||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</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.url }}"
|
||||
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").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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
152
music/templates/music/song_review_detail.html
Normal file
152
music/templates/music/song_review_detail.html
Normal file
|
@ -0,0 +1,152 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:article:author" content="{{ review.owner.username }}">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
|
||||
<title>{% trans 'NiceDB - 评论详情' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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="review-head">
|
||||
<h5 class="review-head__title">
|
||||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<div class="review-head__body">
|
||||
<div class="review-head__info">
|
||||
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
|
||||
|
||||
{% if mark %}
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<span class="review-head__time">{{ review.edited_time }}</span>
|
||||
|
||||
</div>
|
||||
<div class="review-head__actions">
|
||||
{% if request.user == review.owner %}
|
||||
<a class="review-head__action-link" href="{% url '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 }}
|
||||
</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.url }}" 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").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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
$(".markdownx textarea").hide();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
158
music/templates/music/song_review_list.html
Normal file
158
music/templates/music/song_review_list.html
Normal file
|
@ -0,0 +1,158 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的评论' %}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.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.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><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.url }}" 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").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>
|
||||
|
||||
|
||||
{% comment %}
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% endcomment %}
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
3
music/tests.py
Normal file
3
music/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
40
music/urls.py
Normal file
40
music/urls.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from django.urls import 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/<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'),
|
||||
path('song/<int:song_id>/review/list/',
|
||||
retrieve_song_review_list, name='retrieve_song_review_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('album/mark/', create_update_album_mark, name='create_update_album_mark'),
|
||||
path('album/<int:album_id>/mark/list/',
|
||||
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'),
|
||||
]
|
1182
music/views.py
Normal file
1182
music/views.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -118,8 +118,7 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
|
|
@ -122,8 +122,7 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
|
289
users/templates/users/music_list.html
Normal file
289
users/templates/users/music_list.html
Normal file
|
@ -0,0 +1,289 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<script src="{% static 'js/mastodon.js' %}"></script>
|
||||
<script src="{% static 'js/home.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="grid grid--reverse-order">
|
||||
<div class="grid__main grid__main--reverse-order">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="entity-list">
|
||||
|
||||
<div class="set">
|
||||
<h5 class="entity-list__title">
|
||||
{{ user.username }}{{ list_title }}
|
||||
</h5>
|
||||
</div>
|
||||
<ul class="entity-list__entities">
|
||||
|
||||
{% for mark in marks %}
|
||||
|
||||
{% with mark.music as music %}
|
||||
|
||||
<li class="entity-list__entity">
|
||||
<div class="entity-list__entity-img-wrapper">
|
||||
{% if music.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% elif music.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="entity-list__entity-text">
|
||||
<div class="entity-list__entity-title">
|
||||
{% if music.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
|
||||
{{ music.title }}
|
||||
</a>
|
||||
{% elif music.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
|
||||
{{ music.title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ music.source_url }}">
|
||||
<span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<span class="entity-list__entity-info ">
|
||||
{% if music.artist %}{% trans '艺术家' %}
|
||||
{% for artist in music.artist %}
|
||||
<span>{{ artist }}</span>
|
||||
{% if not forloop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if music.genre %}/ {% trans '流派' %}
|
||||
{{ music.genre }}
|
||||
{% endif %}
|
||||
|
||||
{% if music.release_date %}/ {% trans '发行日期' %}
|
||||
{{ music.release_date }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if music.brief %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{{ music.brief }}
|
||||
</p>
|
||||
{% elif music.category_name|lower == 'album' %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '曲目:' %}{{ music.track_list }}
|
||||
</p>
|
||||
{% else %}
|
||||
<!-- song -->
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '所属专辑:' %}{{ music.album }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in music.tag_list %}
|
||||
{% for k, v in tag_dict.items %}
|
||||
{% if k == 'content' %}
|
||||
<span class="tag-collection__tag">
|
||||
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="dividing-line dividing-line--dashed"></div>
|
||||
<div class="entity-marks" style="margin-bottom: 0;">
|
||||
<ul class="entity-marks__mark-list">
|
||||
<li class="entity-marks__mark">
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
|
||||
{% if mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
||||
{% endwith %}
|
||||
|
||||
{% empty %}
|
||||
<div>{% trans '无结果' %}</div>
|
||||
{% endfor %}
|
||||
<!-- user mark -->
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
|
||||
{% if marks.pagination.has_prev %}
|
||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</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 grid__aside--reverse-order grid__aside--tablet-column">
|
||||
<div class="aside-section-wrapper aside-section-wrapper--no-margin">
|
||||
<div class="user-profile" id="userInfoCard">
|
||||
<div class="user-profile__header">
|
||||
<!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
|
||||
<img src="" class="user-profile__avatar mast-avatar">
|
||||
<a href="{% url 'users:home' user.id %}">
|
||||
<h5 class="user-profile__username mast-displayname"></h5>
|
||||
</a>
|
||||
</div>
|
||||
<p class="user-profile__bio mast-brief"></p>
|
||||
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||
|
||||
{% if request.user != user %}
|
||||
<a href="{% url 'users:report' %}?user_id={{ user.id }}"
|
||||
class="user-profile__report-link">{% trans '举报用户' %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relation-dropdown">
|
||||
<div class="relation-dropdown__button">
|
||||
<span class="icon-arrow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
|
||||
<path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="relation-dropdown__body">
|
||||
<div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
|
||||
|
||||
<div class="user-relation" id="followings">
|
||||
<h5 class="user-relation__label">
|
||||
{% trans '关注的人' %}
|
||||
</h5>
|
||||
<a href="{% url 'users:following' user.id %}"
|
||||
class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
|
||||
<ul class="user-relation__related-user-list mast-following">
|
||||
<li class="user-relation__related-user">
|
||||
<a>
|
||||
<img src="" alt="" class="user-relation__related-user-avatar">
|
||||
<div class="user-relation__related-user-name mast-displayname">
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="user-relation" id="followers">
|
||||
<h5 class="user-relation__label">
|
||||
{% trans '被他们关注' %}
|
||||
</h5>
|
||||
<a href="{% url 'users:followers' user.id %}"
|
||||
class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
|
||||
<ul class="user-relation__related-user-list mast-followers">
|
||||
<li class="user-relation__related-user">
|
||||
<a>
|
||||
<img src="" alt="" class="user-relation__related-user-avatar">
|
||||
<div class="user-relation__related-user-name mast-displayname">
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
{% if user == request.user %}
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% else %}
|
||||
<div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
|
||||
{% endif %}
|
||||
<div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
|
||||
<div id="spinner" hidden>
|
||||
<div class="spinner">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -19,7 +19,7 @@
|
|||
<img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
|
||||
|
||||
<div id="loginButton">
|
||||
<p>欢迎来到NiceDB书影音(其实现在只有书汗电影)!</p>
|
||||
<p>欢迎来到NiceDB书影音!</p>
|
||||
<p>
|
||||
NiceDB书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
|
||||
这里仍是一片处女地,丰富的内容需要大家共同创造。
|
||||
|
|
|
@ -13,6 +13,7 @@ urlpatterns = [
|
|||
path('<int:id>/following/', following, name='following'),
|
||||
path('<int:id>/book/<str:status>/', book_list, name='book_list'),
|
||||
path('<int:id>/movie/<str:status>/', movie_list, name='movie_list'),
|
||||
path('<int:id>/music/<str:status>/', music_list, name='music_list'),
|
||||
path('<str:id>/', home, name='home'),
|
||||
path('<str:id>/followers/', followers, name='followers'),
|
||||
path('<str:id>/following/', following, name='following'),
|
||||
|
|
|
@ -17,8 +17,10 @@ from common.models import MarkStatusEnum
|
|||
from common.utils import PageLinksGenerator
|
||||
from books.models import *
|
||||
from movies.models import *
|
||||
from music.models import *
|
||||
from books.forms import BookMarkStatusTranslator
|
||||
from movies.forms import MovieMarkStatusTranslator
|
||||
from music.forms import MusicMarkStatusTranslator
|
||||
from mastodon.models import MastodonApplication
|
||||
|
||||
|
||||
|
@ -108,6 +110,14 @@ def register(request):
|
|||
elif request.method == 'POST':
|
||||
token = request.session['new_user_token']
|
||||
user_data = get_user_data(request.COOKIES['mastodon_domain'], token)
|
||||
if user_data is None:
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': _("长毛象访问失败😫")
|
||||
}
|
||||
)
|
||||
new_user = User(
|
||||
username=user_data['username'],
|
||||
mastodon_id=user_data['id'],
|
||||
|
@ -372,7 +382,7 @@ def book_list(request, id, status):
|
|||
mark.book.tag_list = mark.book.get_tags_manager().values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
|
||||
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书"))
|
||||
list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的书"))
|
||||
return render(
|
||||
request,
|
||||
'users/book_list.html',
|
||||
|
@ -441,7 +451,7 @@ def movie_list(request, id, status):
|
|||
mark.movie.tag_list = mark.movie.get_tags_manager().values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
|
||||
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集"))
|
||||
list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的电影和剧集"))
|
||||
return render(
|
||||
request,
|
||||
'users/movie_list.html',
|
||||
|
@ -455,6 +465,86 @@ def movie_list(request, id, status):
|
|||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@mastodon_request_included
|
||||
@login_required
|
||||
def music_list(request, id, status):
|
||||
if request.method == 'GET':
|
||||
if not status.upper() in MarkStatusEnum.names:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if isinstance(id, str):
|
||||
try:
|
||||
username = id.split('@')[0]
|
||||
site = id.split('@')[1]
|
||||
except IndexError as e:
|
||||
return HttpResponseBadRequest("Invalid user id")
|
||||
query_kwargs = {'username': username, 'mastodon_site': site}
|
||||
elif isinstance(id, int):
|
||||
query_kwargs = {'pk': id}
|
||||
try:
|
||||
user = User.objects.get(**query_kwargs)
|
||||
except ObjectDoesNotExist:
|
||||
msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!")
|
||||
sec_msg = _("目前只开放本站用户注册")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
'secondary_msg': sec_msg,
|
||||
}
|
||||
)
|
||||
if not user == request.user:
|
||||
# mastodon request
|
||||
relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
|
||||
if relation['blocked_by']:
|
||||
msg = _("你没有访问TA主页的权限😥")
|
||||
return render(
|
||||
request,
|
||||
'common/error.html',
|
||||
{
|
||||
'msg': msg,
|
||||
}
|
||||
)
|
||||
queryset = list(AlbumMark.get_available_by_user(user, relation['following']).filter(
|
||||
status=MarkStatusEnum[status.upper()])) \
|
||||
+ list(SongMark.get_available_by_user(user, relation['following']).filter(
|
||||
status=MarkStatusEnum[status.upper()]))
|
||||
|
||||
user.target_site_id = get_cross_site_id(
|
||||
user, request.user.mastodon_site, request.session['oauth_token'])
|
||||
else:
|
||||
queryset = list(AlbumMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) \
|
||||
+ list(SongMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]))
|
||||
queryset = sorted(queryset, key=lambda e: e.edited_time, reverse=True)
|
||||
paginator = Paginator(queryset, ITEMS_PER_PAGE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
marks = paginator.get_page(page_number)
|
||||
for mark in marks:
|
||||
if mark.__class__ == AlbumMark:
|
||||
mark.music = mark.album
|
||||
mark.music.tag_list = mark.album.get_tags_manager().values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
|
||||
elif mark.__class__ == SongMark:
|
||||
mark.music = mark.song
|
||||
mark.music.tag_list = mark.song.get_tags_manager().values('content').annotate(
|
||||
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
|
||||
|
||||
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||
list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的音乐"))
|
||||
return render(
|
||||
request,
|
||||
'users/music_list.html',
|
||||
{
|
||||
'marks': marks,
|
||||
'user': user,
|
||||
'list_title' : list_title,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def report(request):
|
||||
if request.method == 'GET':
|
||||
|
|
Loading…
Add table
Reference in a new issue