From 84359ec4faf274fe7ce15d8ca65d10ee5ec15408 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Dec 2021 15:55:16 -0500 Subject: [PATCH] wip --- collection/forms.py | 11 +- collection/models.py | 30 ++++- collection/templates/create_update.html | 22 +--- collection/templates/detail.html | 83 +++++++++++++- collection/templates/list.html | 105 +++++++++++++++++ collection/urls.py | 7 +- collection/views.py | 144 +++++++++++++++++++++--- common/models.py | 3 + users/templates/users/home.html | 32 ++++++ users/urls.py | 2 + users/views.py | 35 +++++- 11 files changed, 423 insertions(+), 51 deletions(-) create mode 100644 collection/templates/list.html diff --git a/collection/forms.py b/collection/forms.py index 0aa317d2..4db1e885 100644 --- a/collection/forms.py +++ b/collection/forms.py @@ -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(), } diff --git a/collection/models.py b/collection/models.py index a1a89a61..936032f8 100644 --- a/collection/models.py +++ b/collection/models.py @@ -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) diff --git a/collection/templates/create_update.html b/collection/templates/create_update.html index d2d69cb3..0189e566 100644 --- a/collection/templates/create_update.html +++ b/collection/templates/create_update.html @@ -17,39 +17,23 @@
+ {% include "partial/_navbar.html" %}
- {% include "partial/_navbar.html" %} -
{% 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 }}
+ {{ form.media }}
-
{% include "partial/_footer.html" %}
- {% comment %} diff --git a/collection/templates/detail.html b/collection/templates/detail.html index f20c9d43..9a4f06ce 100644 --- a/collection/templates/detail.html +++ b/collection/templates/detail.html @@ -18,12 +18,13 @@ - {{ site_name }} {% trans '收藏單' %} - {{ collection.title }} + {{ site_name }} {% trans '收藏单' %} - {{ collection.title }} + @@ -67,6 +68,78 @@ {{ form.media }} +
+ + +
+ @@ -102,12 +175,14 @@ {% endcomment %} - + diff --git a/collection/templates/list.html b/collection/templates/list.html new file mode 100644 index 00000000..e6f73166 --- /dev/null +++ b/collection/templates/list.html @@ -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 %} + + + + + + + {{ site_name }} - {{ request.user.mastodon_username }}{% trans '的收藏' %} + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+
+ {{ request.user.mastodon_username }}{% trans '的收藏单' %} +
+
    + + {% for collection in collections %} + +
  • + + {{ collection.title }} + {{ collection.edited_time }} + {% if collection.visibility > 0 %} + + {% endif %} +
  • + {% empty %} +
    {% trans '无结果' %}
    + {% endfor %} + +
+
+ +
+
+ + +
+
+
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/collection/urls.py b/collection/urls.py index 99b8d9af..634a710b 100644 --- a/collection/urls.py +++ b/collection/urls.py @@ -4,12 +4,17 @@ from .views import * app_name = 'collection' urlpatterns = [ + path('mine/', list, name='list'), path('create/', create, name='create'), path('/', retrieve, name='retrieve'), path('update//', update, name='update'), path('delete//', delete, name='delete'), path('follow//', follow, name='follow'), - path('unfollow//', follow, name='unfollow'), + path('unfollow//', unfollow, name='unfollow'), + path('/append_item/', append_item, name='append_item'), + path('/delete_item/', delete_item, name='delete_item'), + path('/move_up_item/', move_up_item, name='move_up_item'), + path('/move_down_item/', move_down_item, name='move_down_item'), path('with///', list_with, name='list_with'), # TODO: tag ] diff --git a/collection/views.py b/collection/views.py index 3dfc9729..6e403c22 100644 --- a/collection/views.py +++ b/collection/views.py @@ -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/') + logger.warning('non-GET method at /collections/') 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 diff --git a/common/models.py b/common/models.py index 77d67704..247f6e04 100644 --- a/common/models.py +++ b/common/models.py @@ -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']) diff --git a/users/templates/users/home.html b/users/templates/users/home.html index 0f5e9eb2..b4b7d6bb 100644 --- a/users/templates/users/home.html +++ b/users/templates/users/home.html @@ -531,6 +531,38 @@ +
+
+ {% trans '创建的收藏单' %} +
+ + {{ collections_count }} + + {% if collections_more %} + {% trans '更多' %} + {% endif %} + {% if user == request.user %} + {% trans '新建' %} + {% endif %} + + +
+ {% if user == request.user %} diff --git a/users/urls.py b/users/urls.py index 8d8bc695..4b61fa32 100644 --- a/users/urls.py +++ b/users/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ path('/', home, name='home'), path('/followers/', followers, name='followers'), path('/following/', following, name='following'), + path('/collections/', collection_list, name='collection_list'), path('/book//', book_list, name='book_list'), path('/movie//', movie_list, name='movie_list'), path('/music//', music_list, name='music_list'), @@ -27,6 +28,7 @@ urlpatterns = [ path('/', home, name='home'), path('/followers/', followers, name='followers'), path('/following/', following, name='following'), + path('/collections/', collection_list, name='collection_list'), path('/book//', book_list, name='book_list'), path('/movie//', movie_list, name='movie_list'), path('/music//', music_list, name='music_list'), diff --git a/users/views.py b/users/views.py index 8b21d46f..0147e891 100644 --- a/users/views.py +++ b/users/views.py @@ -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):