This commit is contained in:
Your Name 2021-12-26 15:55:16 -05:00
parent 3875a82381
commit 84359ec4fa
11 changed files with 423 additions and 51 deletions

View file

@ -8,11 +8,9 @@ from common.forms import *
class CollectionForm(forms.ModelForm):
# id = forms.IntegerField(required=False, widget=forms.HiddenInput())
title = forms.CharField(label=_("标题"))
description = MarkdownxFormField(label=_("正文 (Markdown)"))
description = MarkdownxFormField(label=_("详细介绍 (Markdown)"))
share_to_mastodon = forms.BooleanField(
label=_("分享到联邦网络"), initial=True, required=False)
rating = forms.IntegerField(
label=_("评分"), validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
visibility = forms.TypedChoiceField(
label=_("可见性"),
initial=0,
@ -25,16 +23,11 @@ class CollectionForm(forms.ModelForm):
model = Collection
fields = [
'title',
'visibility',
'description',
'cover',
'visibility',
]
widgets = {
# 'name': forms.TextInput(attrs={'placeholder': _("收藏单名称")}),
# 'developer': forms.TextInput(attrs={'placeholder': _("多个开发商使用英文逗号分隔")}),
# 'publisher': forms.TextInput(attrs={'placeholder': _("多个发行商使用英文逗号分隔")}),
# 'genre': forms.TextInput(attrs={'placeholder': _("多个类型使用英文逗号分隔")}),
# 'platform': forms.TextInput(attrs={'placeholder': _("多个平台使用英文逗号分隔")}),
'cover': PreviewImageInput(),
}

View file

@ -22,15 +22,30 @@ class Collection(UserOwnedEntity):
def __str__(self):
return str(self.owner) + ': ' + self.name
@property
def collectionitem_list(self):
return sorted(list(self.collectionitem_set.all()), key=lambda i: i.position)
@property
def item_list(self):
return list(self.collectionitem_set.objects.all()).sort(lambda i: i.position)
return map(lambda i: i.item, self.collectionitem_list)
@property
def plain_description(self):
html = markdown(self.description)
return RE_HTML_TAG.sub(' ', html)
def append_item(self, item, comment=""):
cl = self.collectionitem_list
if item is None or len(list(filter(lambda i: i.item == item, cl))) > 0:
return None
else:
i = CollectionItem(collection=self, position=cl[-1].position + 1 if len(cl) else 1, comment=comment)
print(i)
i.set_item(item)
i.save()
return i
class CollectionItem(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, null=True)
@ -46,3 +61,16 @@ class CollectionItem(models.Model):
def item(self):
items = list(filter(lambda i: i is not None, [self.movie, self.book, self.album, self.song, self.game]))
return items[0] if len(items) > 0 else None
# @item.setter
def set_item(self, new_item):
old_item = self.item
if old_item == new_item:
return
if old_item is not None:
self.movie = None
self.book = None
self.album = None
self.song = None
self.game = None
setattr(self, new_item.__class__.__name__.lower(), new_item)

View file

@ -17,39 +17,23 @@
<body>
<div id="page-wrapper">
{% include "partial/_navbar.html" %}
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
<div class="single-section-wrapper" id="main">
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
{{ form }}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
{{ form.media }}
</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>

View file

@ -18,12 +18,13 @@
<meta property="og:article:author" content="{{ collection.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>{{ site_name }} {% trans '收藏' %} - {{ collection.title }}</title>
<title>{{ site_name }} {% trans '收藏' %} - {{ collection.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>
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<script src="https://unpkg.com/htmx.org@1.6.1"></script>
</head>
<body>
@ -67,6 +68,78 @@
</div>
{{ form.media }}
</div>
<div class="entity-list">
<ul class="entity-list__entities">
{% for item in collection.collectionitem_list %}
{% if item.item is not None %}
<li class="entity-list__entity">
<div class="entity-list__entity-img-wrapper">
<a href="{{ item.item.get_absolute_url }}">
<img src="{{ item.item.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
</a>
</div>
<div class="entity-list__entity-text">
{% if editable %}
<div style="float:right">
{% if not forloop.first %}
<a hx-post="{% url 'collection:move_up_item' form.instance.id item.id %}"></a>
{% endif %}
{% if not forloop.last %}
<a hx-post="{% url 'collection:move_down_item' form.instance.id item.id %}"></a>
{% endif %}
<a hx-post="{% url 'collection:delete_item' form.instance.id item.id %}"></a>
</div>
{% endif %}
<div class="entity-list__entity-title">
<a href="{{ item.item.get_absolute_url }}">{{ item.item.title }}</a>
<a href="{{ item.item.source_url }}">
<span class="source-label source-label__{{ item.item.source_site }}">{{ item.item.get_source_site_display }}</span>
</a>
</div>
<span class="entity-list__entity-info entity-list__entity-info--full-length">
<p class="entity-list__entity-brief">
{{ item.item.brief }}
</p>
</span>
<div class="tag-collection">
{% for tag_dict in item.item.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">
<p class="entity-marks__mark-content">{{ item.comment }}</p>
</li>
</ul>
</div>
</div>
</li>
{% endif %}
{% empty %}
{% endfor %}
{% if editable %}
<li>
<form action="{% url 'collection:append_item' form.instance.id %}" method="POST">
{% csrf_token %}
<input type="url" name="url" placeholder="https://neodb.social/movies/1/" style="min-width:24rem" required>
<input type="text" name="comment" placeholder="{% trans '备注' %}" style="min-width:24rem">
<input class="button" type="submit" value="{% trans '添加' %}">
</form>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
@ -102,12 +175,14 @@
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
$(".markdownx textarea").hide();
</script>
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
})
</script>
</body>

View file

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

View file

@ -4,12 +4,17 @@ from .views import *
app_name = 'collection'
urlpatterns = [
path('mine/', list, name='list'),
path('create/', create, name='create'),
path('<int:id>/', retrieve, name='retrieve'),
path('update/<int:id>/', update, name='update'),
path('delete/<int:id>/', delete, name='delete'),
path('follow/<int:id>/', follow, name='follow'),
path('unfollow/<int:id>/', follow, name='unfollow'),
path('unfollow/<int:id>/', unfollow, name='unfollow'),
path('<int:id>/append_item/', append_item, name='append_item'),
path('<int:id>/delete_item/<int:item_id>', delete_item, name='delete_item'),
path('<int:id>/move_up_item/<int:item_id>', move_up_item, name='move_up_item'),
path('<int:id>/move_down_item/<int:item_id>', move_down_item, name='move_down_item'),
path('with/<str:type>/<int:id>/', list_with, name='list_with'),
# TODO: tag
]

View file

@ -18,6 +18,10 @@ from common.models import SourceSiteEnum
from .models import *
from .forms import *
from django.conf import settings
import re
from users.models import User
from django.http import HttpResponseRedirect
logger = logging.getLogger(__name__)
@ -36,6 +40,13 @@ REVIEW_PER_PAGE = 20
TAG_NUMBER = 10
class HTTPResponseHXRedirect(HttpResponseRedirect):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self['HX-Redirect'] = self['Location']
status_code = 200
# public data
###########################
@login_required
@ -73,7 +84,7 @@ def create(request):
'create_update.html',
{
'form': form,
'title': _('添加收藏'),
'title': _('添加收藏'),
'submit_url': reverse("collection:create"),
# provided for frontend js
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
@ -87,7 +98,7 @@ def create(request):
@login_required
def update(request, id):
page_title = _("修改游戏")
page_title = _("修改收藏单")
collection = get_object_or_404(Collection, pk=id)
if not collection.is_visible_to(request.user):
raise PermissionDenied()
@ -112,10 +123,6 @@ def update(request, id):
try:
with transaction.atomic():
form.save()
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
real_url = form.instance.get_absolute_url()
form.instance.source_url = real_url
form.instance.save()
except IntegrityError as e:
logger.error(e.__str__())
return HttpResponseServerError("integrity error")
@ -131,7 +138,7 @@ def update(request, id):
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
}
)
return redirect(reverse("games:retrieve", args=[form.instance.id]))
return redirect(reverse("collection:retrieve", args=[form.instance.id]))
else:
return HttpResponseBadRequest()
@ -156,32 +163,31 @@ def retrieve(request, id):
{
'collection': collection,
'form': form,
'editable': collection.is_editable_by(request.user),
'followers': followers,
}
)
else:
logger.warning('non-GET method at /games/<id>')
logger.warning('non-GET method at /collections/<id>')
return HttpResponseBadRequest()
@permission_required("games.delete_game")
@permission_required("collections.delete_collection")
@login_required
def delete(request, id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'GET':
game = get_object_or_404(Game, pk=id)
return render(
request,
'games/delete.html',
'collections/delete.html',
{
'game': game,
'collection': collection,
}
)
elif request.method == 'POST':
if request.user.is_staff:
# only staff has right to delete
game = get_object_or_404(Game, pk=id)
game.delete()
if request.user.is_staff or request.user == collection.owner:
collection.delete()
return redirect(reverse("common:home"))
else:
raise PermissionDenied()
@ -199,6 +205,112 @@ def unfollow(request, id):
pass
@login_required
def list(request, user_id=None):
if request.method == 'GET':
queryset = Collection.objects.filter(owner=request.user if user_id is None else User.objects.get(id=user_id))
paginator = Paginator(queryset, REVIEW_PER_PAGE)
page_number = request.GET.get('page', default=1)
reviews = paginator.get_page(page_number)
reviews.pagination = PageLinksGenerator(
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
return render(
request,
'list.html',
{
'collections': queryset,
}
)
else:
return HttpResponseBadRequest()
def get_entity_by_url(url):
m = re.findall(r'^/?(movies|books|games|music/album|music/song)/(\d+)/?', url.lower().replace(settings.APP_WEBSITE.lower(), ''))
if len(m) > 0:
mapping = {
'movies': Movie,
'books': Book,
'games': Game,
'music/album': Album,
'music/song': Song,
}
cls = mapping.get(m[0][0])
id = int(m[0][1])
if cls is not None:
return cls.objects.get(id=id)
return None
@login_required
def append_item(request, id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
url = request.POST.get('url')
comment = request.POST.get('comment')
item = get_entity_by_url(url)
collection.append_item(item, comment)
collection.save()
return redirect(reverse("collection:retrieve", args=[id]))
else:
return HttpResponseBadRequest()
@login_required
def delete_item(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
item.delete()
# collection.save()
return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id]))
return HttpResponseBadRequest()
@login_required
def move_up_item(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
items = collection.collectionitem_list
idx = items.index(item)
if idx > 0:
o = items[idx - 1]
p = o.position
o.position = item.position
item.position = p
o.save()
item.save()
# collection.save()
return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id]))
return HttpResponseBadRequest()
@login_required
def move_down_item(request, id, item_id):
collection = get_object_or_404(Collection, pk=id)
if request.method == 'POST' and collection.is_editable_by(request.user):
# item_id = int(request.POST.get('item_id'))
item = CollectionItem.objects.get(id=item_id)
if item is not None and item.collection == collection:
items = collection.collectionitem_list
idx = items.index(item)
if idx + 1 < len(items):
o = items[idx + 1]
p = o.position
o.position = item.position
item.position = p
o.save()
item.save()
# collection.save()
return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id]))
return HttpResponseBadRequest()
@login_required
def list_with(request, type, id):
pass

View file

@ -174,6 +174,9 @@ class UserOwnedEntity(models.Model):
else:
return True
def is_editable_by(self, viewer):
return True if viewer.is_staff or viewer.is_superuser or viewer == self.owner else False
@classmethod
def get_available(cls, entity, request_user, following_only=False):
# e.g. SongMark.get_available(song, request.user, request.session['oauth_token'])

View file

@ -531,6 +531,38 @@
</ul>
</div>
<div class="entity-sort" id="collectionCreated">
<h5 class="entity-sort__label">
{% trans '创建的收藏单' %}
</h5>
<span class="entity-sort__count">
{{ collections_count }}
</span>
{% if collections_more %}
<a href="{% url 'users:collection_list' user.mastodon_username %}"
class="entity-sort__more-link">{% trans '更多' %}</a>
{% endif %}
{% if user == request.user %}
<a href="{% url 'collection:create' %}"class="entity-sort__more-link">{% trans '新建' %}</a>
{% endif %}
<ul class="entity-sort__entity-list">
{% for collection in collections %}
<li class="entity-sort__entity">
<a href="{% url 'collection:retrieve' collection.id %}">
<img src="{{ collection.cover|thumb:'normal' }}"
alt="{{collection.title}}" class="entity-sort__entity-img">
<span class="entity-sort__entity-name"
title="{{collection.title}}">{{ collection.title }}</span>
</a>
</li>
{% empty %}
<div>暂无记录</div>
{% endfor %}
</ul>
</div>
</div>
{% if user == request.user %}

View file

@ -20,6 +20,7 @@ urlpatterns = [
path('<int:id>/', home, name='home'),
path('<int:id>/followers/', followers, name='followers'),
path('<int:id>/following/', following, name='following'),
path('<int:id>/collections/', collection_list, name='collection_list'),
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'),
@ -27,6 +28,7 @@ urlpatterns = [
path('<str:id>/', home, name='home'),
path('<str:id>/followers/', followers, name='followers'),
path('<str:id>/following/', following, name='following'),
path('<str:id>/collections/', collection_list, name='collection_list'),
path('<str:id>/book/<str:status>/', book_list, name='book_list'),
path('<str:id>/movie/<str:status>/', movie_list, name='movie_list'),
path('<str:id>/music/<str:status>/', music_list, name='music_list'),

View file

@ -37,6 +37,7 @@ from books.models import BookMark, BookReview
from movies.models import MovieMark, MovieReview
from games.models import GameMark, GameReview
from music.models import AlbumMark, SongMark, AlbumReview, SongReview
from collection.models import Collection
# Views
@ -301,7 +302,7 @@ def home(request, id):
album_reviews = AlbumReview.get_available_by_user(user, relation['following'])
game_reviews = GameReview.get_available_by_user(user, relation['following'])
collections = Collection.objects.filter(owner=user)
# book marks
filtered_book_marks = filter_marks(book_marks, BOOKS_PER_SET, 'book')
book_marks_count = count_marks(book_marks, "book")
@ -364,6 +365,10 @@ def home(request, id):
'music_reviews_count': len(music_reviews),
'game_reviews_count': game_reviews.count(),
'collections': collections.order_by("-edited_time")[:BOOKS_PER_SET],
'collections_count': collections.count(),
'collections_more': collections.count() > BOOKS_PER_SET,
'layout': layout,
'reports': reports,
'unread_announcements': unread_announcements,
@ -902,6 +907,34 @@ def manage_report(request):
return HttpResponseBadRequest()
@login_required
def collection_list(request, id):
from collection.views import list
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,
}
)
return list(request, user.id)
# Utils
########################################
def refresh_mastodon_data_task(user, token=None):