diff --git a/catalog/search/views.py b/catalog/search/views.py index f9ee2b7c..af50aa7c 100644 --- a/catalog/search/views.py +++ b/catalog/search/views.py @@ -141,7 +141,7 @@ def search(request): { "items": items, "dup_items": dup_items, - "pagination": PageLinksGenerator(PAGE_LINK_NUMBER, p, num_pages), + "pagination": PageLinksGenerator(p, num_pages, request.GET), "sites": SiteName.labels, "hide_category": hide_category, }, diff --git a/catalog/views.py b/catalog/views.py index 11ed0da2..164bdc9e 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -157,7 +157,7 @@ def mark_list(request, item_path, item_uuid, following_only=False): paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE) page_number = request.GET.get("page", default=1) marks = paginator.get_page(page_number) - pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) + pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET) return render( request, "item_mark_list.html", @@ -179,7 +179,7 @@ def review_list(request, item_path, item_uuid): paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE) page_number = request.GET.get("page", default=1) reviews = paginator.get_page(page_number) - pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) + pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET) return render( request, "item_review_list.html", diff --git a/common/static/scss/_common.scss b/common/static/scss/_common.scss index 39790539..c8e41878 100644 --- a/common/static/scss/_common.scss +++ b/common/static/scss/_common.scss @@ -184,3 +184,26 @@ form img { margin: 0 0.5em; } } + +.pagination { + text-align: center; + width: 100%; + margin-top: 1em; + a { + margin: 0 0.3em; + } + .s, .prev, .next { + font-size: 1.4em; + } + .prev { + margin-right: 0.5em; + } + .next { + margin-left: 0.5em; + } + .current { + font-weight: bold; + font-size: 1.1em; + color: var(--pico-secondary); + } +} diff --git a/common/static/scss/_legacy.sass b/common/static/scss/_legacy.sass index 45093622..ed1e6771 100644 --- a/common/static/scss/_legacy.sass +++ b/common/static/scss/_legacy.sass @@ -954,57 +954,10 @@ $panel-padding : 0 padding: 0 list-style: none -.pagination - // position: absolute - // bottom: 30px - // left: 50% - // transform: translateX(-50%) - text-align: center - width: 100% - - & &__page-link - font-weight: normal - margin: 0 5px - - &--current - font-weight: bold - font-size: 1.2em - // text-decoration: underline - color: $color-secondary - - & &__nav-link - font-size: 1.4em - margin: 0 2px - - $nav-link-edge-margin-width: 18px - - &--right-margin - margin-right: $nav-link-edge-margin-width - - &--left-margin - margin-left: $nav-link-edge-margin-width - - &--hidden - display: none - // Small devices (landscape phones, 576px and up) @media (max-width: $small-devices) - .pagination - & &__page-link - margin: 0 3px - - & &__nav-link - font-size: 1.4em - margin: 0 2px - - $nav-link-edge-margin-width: 10px - - &--right-margin - margin-right: $nav-link-edge-margin-width - - &--left-margin - margin-left: $nav-link-edge-margin-width + pass // Medium devices (tablets, 768px and up) @media (max-width: $medium-devices) pass diff --git a/common/templates/_pagination.html b/common/templates/_pagination.html index 892dcb18..729d61f3 100644 --- a/common/templates/_pagination.html +++ b/common/templates/_pagination.html @@ -1,23 +1,20 @@ <div class="pagination"> {% if pagination.has_prev %} - <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page=1" - class="pagination__nav-link pagination__nav-link">«</a> - <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ pagination.previous_page }}" - class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a> + <a href="?{{ pagination.query_string }}page=1" class="s">«</a> + <a href="?{{ pagination.query_string }}page={{ pagination.previous_page }}" + class="prev">‹</a> {% endif %} {% for page in pagination.page_range %} {% if page == pagination.current_page %} - <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}" - class="pagination__page-link pagination__page-link--current">{{ page }}</a> + <a href="?{{ pagination.query_string }}page={{ page }}" class="current">{{ page }}</a> {% else %} - <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}" - class="pagination__page-link">{{ page }}</a> + <a href="?{{ pagination.query_string }}page={{ page }}">{{ page }}</a> {% endif %} {% endfor %} {% if pagination.has_next %} - <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ pagination.next_page }}" - class="pagination__nav-link pagination__nav-link--left-margin">›</a> - <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ pagination.last_page }}" - class="pagination__nav-link">»</a> + <a href="?{{ pagination.query_string }}page={{ pagination.next_page }}" + class="next">›</a> + <a href="?{{ pagination.query_string }}page={{ pagination.last_page }}" + class="s">»</a> {% endif %} </div> diff --git a/common/utils.py b/common/utils.py index b42de4fb..8c6d6191 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,11 +1,15 @@ import functools +import re import uuid from typing import TYPE_CHECKING -from django.http import Http404, HttpRequest, HttpResponseRedirect +from django.db.models import query +from django.http import Http404, HttpRequest, HttpResponseRedirect, QueryDict from django.utils import timezone from django.utils.baseconv import base62 +from .config import PAGE_LINK_NUMBER + if TYPE_CHECKING: from users.models import APIdentity, User @@ -82,8 +86,19 @@ class PageLinksGenerator: length -- the number of page links in pagination """ - def __init__(self, length: int, current_page: int, total_pages: int): + def __init__( + self, current_page: int, total_pages: int, query: QueryDict | None = None + ): + length = PAGE_LINK_NUMBER current_page = int(current_page) + self.query_string = "" + if query: + q = query.copy() + if q.get("page"): + q.pop("page") + self.query_string = q.urlencode() + if self.query_string: + self.query_string += "&" self.current_page = current_page self.previous_page = current_page - 1 if current_page > 1 else None self.next_page = current_page + 1 if current_page < total_pages else None diff --git a/journal/models/rating.py b/journal/models/rating.py index d5813d6d..5bbc94fe 100644 --- a/journal/models/rating.py +++ b/journal/models/rating.py @@ -1,11 +1,12 @@ from datetime import datetime +from typing import Any -from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator -from django.db import connection, models +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models from django.db.models import Avg, Count, Q from django.utils.translation import gettext_lazy as _ -from catalog.models import Item, ItemCategory +from catalog.models import Item from users.models import APIdentity from .common import Content @@ -116,24 +117,22 @@ class Rating(Content): @staticmethod def update_item_rating( - item: Item, owner: APIdentity, rating_grade: int | None, visibility: int = 0 + item: Item, + owner: APIdentity, + rating_grade: int | None, + visibility: int = 0, + created_time: datetime | None = None, ): if rating_grade and (rating_grade < 1 or rating_grade > 10): raise ValueError(f"Invalid rating grade: {rating_grade}") - rating = Rating.objects.filter(owner=owner, item=item).first() if not rating_grade: - if rating: - rating.delete() - rating = None - elif rating is None: - rating = Rating.objects.create( - owner=owner, item=item, grade=rating_grade, visibility=visibility - ) - elif rating.grade != rating_grade or rating.visibility != visibility: - rating.visibility = visibility - rating.grade = rating_grade - rating.save() - return rating + Rating.objects.filter(owner=owner, item=item).delete() + else: + d: dict[str, Any] = {"grade": rating_grade, "visibility": visibility} + if created_time: + d["created_time"] = created_time + r, _ = Rating.objects.update_or_create(owner=owner, item=item, defaults=d) + return r @staticmethod def get_item_rating(item: Item, owner: APIdentity) -> int | None: diff --git a/journal/models/shelf.py b/journal/models/shelf.py index a01db0fe..8d9ced04 100644 --- a/journal/models/shelf.py +++ b/journal/models/shelf.py @@ -259,6 +259,15 @@ class ShelfManager: else: return qs + def get_members( + self, shelf_type: ShelfType, item_category: ItemCategory | None = None + ): + qs = self.shelf_list[shelf_type].members.all() + if item_category: + return qs.filter(q_item_in_category(item_category)) + else: + return qs + # def get_items_on_shelf(self, item_category, shelf_type): # shelf = ( # self.owner.shelf_set.all() diff --git a/journal/templates/_sidebar_user_mark_list.html b/journal/templates/_sidebar_user_mark_list.html index 88339cb6..6cacad7b 100644 --- a/journal/templates/_sidebar_user_mark_list.html +++ b/journal/templates/_sidebar_user_mark_list.html @@ -5,52 +5,65 @@ <section class="filter"> <script> function filter() { - location = '{{ user.identity.url }}' + $('#shelf')[0].value + '/' + $('#category')[0].value + '/' + ( $('#year')[0].value ? $('#year')[0].value + '/' : '' ); + var q = []; + if ($('#year')[0].value) { + q.push('year=' + $('#year')[0].value) + } + if ($('#sort')[0].value) { + q.push('sort=' + $('#sort')[0].value) + } + location = '{{ user.identity.url }}' + $('#shelf')[0].value + '/' + $('#category')[0].value + '/' + (q.length ? '?' + q.join('&') : '') } - </script> - <form role="search" method="get" action=""> - <select name="year" id="year" onchange="filter()"> - <option value="">全部</option> - {% for y in years %} - <option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option> - {% endfor %} - </select> - <select name="shelf" id="shelf" onchange="filter()"> - {% for typ, label in shelf_labels %} - {% if label %} - <option {% if typ in request.path %}selected{% endif %} value="{{ typ }}">{{ label }}</option> + <form method="get" action=""> + <fieldset role="search" style="width: 100%; padding;"> + <select name="shelf" id="shelf" onchange="filter()"> + {% for typ, label in shelf_labels %} + {% if label %} + <option {% if typ in request.path %}selected{% endif %} value="{{ typ }}">{{ label }}</option> + {% endif %} + {% endfor %} + <option {% if '/reviews/' in request.path %}selected{% endif %} + value="reviews">评论</option> + </select> + <select name="category" id="category" onchange="filter()"> + {% visible_categories as cats %} + {% if 'book' in cats %} + <option {% if '/book/' in request.path %}selected{% endif %} value="book">书籍</option> {% endif %} - {% endfor %} - <option {% if '/reviews/' in request.path %}selected{% endif %} - value="reviews">评论</option> - </select> - <select name="category" id="category" onchange="filter()"> - {% visible_categories as cats %} - {% if 'book' in cats %} - <option {% if '/book/' in request.path %}selected{% endif %} value="book">书籍</option> - {% endif %} - {% if 'movie' in cats %} - <option {% if '/movie/' in request.path %}selected{% endif %} value="movie">电影</option> - {% endif %} - {% if 'tv' in cats %} - <option {% if '/tv/' in request.path %}selected{% endif %} value="tv">剧集</option> - {% endif %} - {% if 'podcast' in cats %} - <option {% if '/podcast/' in request.path %}selected{% endif %} - value="podcast">播客</option> - {% endif %} - {% if 'music' in cats %} - <option {% if '/music/' in request.path %}selected{% endif %} value="music">音乐</option> - {% endif %} - {% if 'game' in cats %} - <option {% if '/game/' in request.path %}selected{% endif %} value="game">游戏</option> - {% endif %} - {% if 'performance' in cats %} - <option {% if '/performance/' in request.path %}selected{% endif %} - value="performance">演出</option> - {% endif %} - </select> + {% if 'movie' in cats %} + <option {% if '/movie/' in request.path %}selected{% endif %} value="movie">电影</option> + {% endif %} + {% if 'tv' in cats %} + <option {% if '/tv/' in request.path %}selected{% endif %} value="tv">剧集</option> + {% endif %} + {% if 'podcast' in cats %} + <option {% if '/podcast/' in request.path %}selected{% endif %} + value="podcast">播客</option> + {% endif %} + {% if 'music' in cats %} + <option {% if '/music/' in request.path %}selected{% endif %} value="music">音乐</option> + {% endif %} + {% if 'game' in cats %} + <option {% if '/game/' in request.path %}selected{% endif %} value="game">游戏</option> + {% endif %} + {% if 'performance' in cats %} + <option {% if '/performance/' in request.path %}selected{% endif %} + value="performance">演出</option> + {% endif %} + </select> + <select name="year" id="year" onchange="filter()"> + <option value="">全部</option> + {% for y in years %} + <option value="{{ y }}" {% if y == year %}selected{% endif %}>{{ y }}</option> + {% endfor %} + </select> + <select name="sort" id="sort" onchange="filter()"> + <option value="" disabled>排列顺序</option> + <option value="">时间</option> + <option value="rating" {% if 'rating' == sort %}selected{% endif %}>评分</option> + </select> + </fieldset> </form> <!-- <style type="text/css"> diff --git a/journal/urls.py b/journal/urls.py index 630072fd..841bb831 100644 --- a/journal/urls.py +++ b/journal/urls.py @@ -101,15 +101,6 @@ urlpatterns = [ user_mark_list, name="user_mark_list", ), - re_path( - r"^users/(?P<user_name>[~A-Za-z0-9_\-.@]+)/(?P<shelf_type>" - + _get_all_shelf_types() - + ")/(?P<item_category>" - + _get_all_categories() - + r")/(?P<year>\d+)/$", - user_mark_list, - name="user_mark_list_year", - ), re_path( r"^users/(?P<user_name>[~A-Za-z0-9_\-.@]+)/reviews/(?P<item_category>" + _get_all_categories() @@ -117,13 +108,6 @@ urlpatterns = [ user_review_list, name="user_review_list", ), - re_path( - r"^users/(?P<user_name>[~A-Za-z0-9_\-.@]+)/reviews/(?P<item_category>" - + _get_all_categories() - + r")/(?P<year>\d+)/$", - user_review_list, - name="user_review_list_year", - ), re_path( r"^users/(?P<user_name>[~A-Za-z0-9_\-.@]+)/tags/(?P<tag_title>.+)/$", user_tag_member_list, diff --git a/journal/views/common.py b/journal/views/common.py index 69cb4e94..46ec9f6c 100644 --- a/journal/views/common.py +++ b/journal/views/common.py @@ -3,7 +3,7 @@ import datetime from django.contrib.auth.decorators import login_required from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied from django.core.paginator import Paginator -from django.db.models import Min +from django.db.models import F, Min, OuterRef, Subquery from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -57,12 +57,15 @@ def render_list( item_category=None, tag_title=None, year=None, + sort="time", ): target = request.target_identity viewer = request.user.identity tag = None + sort = request.GET.get("sort") + year = request.GET.get("year") if type == "mark" and shelf_type: - queryset = target.shelf_manager.get_latest_members(shelf_type, item_category) + queryset = target.shelf_manager.get_members(shelf_type, item_category) elif type == "tagmember": tag = Tag.objects.filter(owner=target, title=tag_title).first() if not tag: @@ -74,6 +77,15 @@ def render_list( queryset = Review.objects.filter(q_item_in_category(item_category)) else: raise BadRequest() + if sort == "rating": + rating = Rating.objects.filter( + owner_id=OuterRef("owner_id"), item_id=OuterRef("item_id") + ) + queryset = queryset.alias( + rating_grade=Subquery(rating.values("grade")) + ).order_by(F("rating_grade").desc(nulls_last=True)) + else: + queryset = queryset.order_by("-created_time") start_date = queryset.aggregate(Min("created_time"))["created_time__min"] if start_date: start_year = start_date.year @@ -81,16 +93,14 @@ def render_list( years = reversed(range(start_year, current_year + 1)) else: years = [] - queryset = queryset.filter( - q_owned_piece_visible_to_user(request.user, target) - ).order_by("-created_time") + queryset = queryset.filter(q_owned_piece_visible_to_user(request.user, target)) if year: year = int(year) queryset = queryset.filter(created_time__year=year) paginator = Paginator(queryset, PAGE_SIZE) page_number = int(request.GET.get("page", default=1)) members = paginator.get_page(page_number) - pagination = PageLinksGenerator(PAGE_SIZE, page_number, paginator.num_pages) + pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET) shelf_labels = get_shelf_labels_for_category(item_category) if item_category else [] return render( request, @@ -103,6 +113,7 @@ def render_list( "pagination": pagination, "years": years, "year": year, + "sort": sort, "shelf": shelf_type, "shelf_labels": shelf_labels, "category": item_category, diff --git a/journal/views/mark.py b/journal/views/mark.py index ab97ee46..a09e749e 100644 --- a/journal/views/mark.py +++ b/journal/views/mark.py @@ -11,7 +11,7 @@ from django.utils.dateparse import parse_datetime from django.utils.translation import gettext_lazy as _ from catalog.models import * -from common.utils import AuthedHttpRequest, PageLinksGenerator, get_uuid_or_404 +from common.utils import AuthedHttpRequest, get_uuid_or_404 from mastodon.api import boost_toot_later, share_comment from takahe.utils import Takahe @@ -184,14 +184,7 @@ def comment(request: AuthedHttpRequest, item_uuid): raise BadRequest() -def user_mark_list( - request: AuthedHttpRequest, user_name, shelf_type, item_category, year=None -): +def user_mark_list(request: AuthedHttpRequest, user_name, shelf_type, item_category): return render_list( - request, - user_name, - "mark", - shelf_type=shelf_type, - item_category=item_category, - year=year, + request, user_name, "mark", shelf_type=shelf_type, item_category=item_category ) diff --git a/journal/views/post.py b/journal/views/post.py index 13ac4322..f6959b90 100644 --- a/journal/views/post.py +++ b/journal/views/post.py @@ -5,12 +5,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods -from common.utils import ( - AuthedHttpRequest, - PageLinksGenerator, - get_uuid_or_404, - target_identity_required, -) +from common.utils import AuthedHttpRequest, get_uuid_or_404, target_identity_required from mastodon.api import boost_toot_later from takahe.utils import Takahe diff --git a/journal/views/review.py b/journal/views/review.py index 24e75a08..4ce97a90 100644 --- a/journal/views/review.py +++ b/journal/views/review.py @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods from catalog.models import * -from common.utils import AuthedHttpRequest, PageLinksGenerator, get_uuid_or_404 +from common.utils import AuthedHttpRequest, get_uuid_or_404 from journal.models.renderers import convert_leading_space_in_md, render_md from users.models.apidentity import APIdentity @@ -97,10 +97,8 @@ def review_edit(request: AuthedHttpRequest, item_uuid, review_uuid=None): raise BadRequest() -def user_review_list(request, user_name, item_category, year=None): - return render_list( - request, user_name, "review", item_category=item_category, year=None - ) +def user_review_list(request, user_name, item_category): + return render_list(request, user_name, "review", item_category=item_category) MAX_ITEM_PER_TYPE = 10 diff --git a/misc/www/robots.txt b/misc/www/robots.txt index 80eef49a..dc2450ff 100644 --- a/misc/www/robots.txt +++ b/misc/www/robots.txt @@ -1,2 +1,14 @@ +User-agent: CCBot +Disallow: /review/ + User-agent: GPTBot Disallow: /review/ + +User-agent: Google-Extended +Disallow: /review/ + +User-agent: FacebookBot +Disallow: /review/ + +User-agent: Omgilibot +Disallow: /review/