From a5d73e864eba4c669dbd298329910f671b4a4fb0 Mon Sep 17 00:00:00 2001 From: Your Name <you@example.com> Date: Wed, 12 Jul 2023 01:11:15 -0400 Subject: [PATCH] hide search category --- catalog/api.py | 5 +- catalog/search/models.py | 8 +-- catalog/search/typesense.py | 9 +-- catalog/search/views.py | 23 ++++++- catalog/templates/search_results.html | 88 +++++++++++++++----------- common/templates/_header.html | 38 +++++++---- common/templatetags/duration.py | 12 ++++ doc/catalog.md | 2 +- users/account.py | 7 ++ users/data.py | 3 + users/models.py | 8 ++- users/templates/users/preferences.html | 13 ++++ 12 files changed, 149 insertions(+), 67 deletions(-) diff --git a/catalog/api.py b/catalog/api.py index 89a1dbc3..012060ea 100644 --- a/catalog/api.py +++ b/catalog/api.py @@ -45,7 +45,10 @@ def search_item( if not query: return 400, {"message": "Invalid query"} items, num_pages, count, _ = query_index( - query, page=page, category=category, prepare_external=False + query, + page=page, + categories=[category] if category else None, + prepare_external=False, ) return 200, {"data": items, "pages": num_pages, "count": count} diff --git a/catalog/search/models.py b/catalog/search/models.py index 210b347a..23025963 100644 --- a/catalog/search/models.py +++ b/catalog/search/models.py @@ -18,7 +18,7 @@ _logger = logging.getLogger(__name__) class DbIndexer: @classmethod - def search(cls, q, page=1, category=None, tag=None, sort=None): + def search(cls, q, page=1, categories=None, tag=None, sort=None): result = lambda: None result.items = Item.objects.filter(title__contains=q)[:10] result.num_pages = 1 @@ -47,14 +47,14 @@ else: Indexer = DbIndexer -def query_index(keywords, category=None, tag=None, page=1, prepare_external=True): +def query_index(keywords, categories=None, tag=None, page=1, prepare_external=True): if ( page < 1 or page > 99 or (not tag and isinstance(keywords, str) and len(keywords) < 2) ): return [], 0, 0, [] - result = Indexer.search(keywords, page=page, category=category, tag=tag) + result = Indexer.search(keywords, page=page, categories=categories, tag=tag) keys = set() items = [] duplicated_items = [] @@ -92,7 +92,7 @@ def query_index(keywords, category=None, tag=None, page=1, prepare_external=True if prepare_external: # store site url to avoid dups in external search - cache_key = f"search_{category}_{keywords}" + cache_key = f"search_{','.join(categories or [])}_{keywords}" urls = list(set(cache.get(cache_key, []) + urls)) cache.set(cache_key, urls, timeout=300) diff --git a/catalog/search/typesense.py b/catalog/search/typesense.py index 98459d46..b636bcd4 100644 --- a/catalog/search/typesense.py +++ b/catalog/search/typesense.py @@ -311,13 +311,10 @@ class Indexer: logger.warn(f"delete item error: \n{e}") @classmethod - def search(cls, q, page=1, category=None, tag=None, sort=None): + def search(cls, q, page=1, categories=None, tag=None, sort=None): f = [] - if category: - if category == "movietv": - f.append("category:= [movie,tv]") - else: - f.append("category:= " + category) + if categories: + f.append(f"category:= [{','.join(categories)}]") if tag: f.append(f"tags:= '{tag}'") filters = " && ".join(f) diff --git a/catalog/search/views.py b/catalog/search/views.py index b88cebc5..41224e75 100644 --- a/catalog/search/views.py +++ b/catalog/search/views.py @@ -5,7 +5,7 @@ from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.utils.translation import gettext_lazy as _ from django.http import HttpResponseRedirect -from catalog.common.models import SiteName +from catalog.common.models import ItemCategory, SiteName from catalog.common.sites import AbstractSite, SiteManager from ..models import * from django.conf import settings @@ -82,10 +82,27 @@ def fetch(request, url, is_refetch: bool = False, site: AbstractSite | None = No ) +def visible_categories(request): + vc = request.session.get("p_categories", None) + if vc is None: + vc = [ + x + for x in item_categories() + if x.value not in request.user.preference.hidden_categories + ] + request.session["p_categories"] = vc + return vc + + def search(request): category = request.GET.get("c", default="all").strip().lower() - if category == "all": + if category == "all" or not category: category = None + categories = visible_categories(request) + elif category == "movietv": + categories = [ItemCategory.Movie, ItemCategory.TV] + else: + categories = [ItemCategory(category)] keywords = request.GET.get("q", default="").strip() tag = request.GET.get("tag", default="").strip() p = request.GET.get("page", default="1") @@ -105,7 +122,7 @@ def search(request): if site: return fetch(request, keywords, False, site) - items, num_pages, _, dup_items = query_index(keywords, category, tag, p) + items, num_pages, _, dup_items = query_index(keywords, categories, tag, p) return render( request, "search_results.html", diff --git a/catalog/templates/search_results.html b/catalog/templates/search_results.html index 42621785..2648ed3f 100644 --- a/catalog/templates/search_results.html +++ b/catalog/templates/search_results.html @@ -4,6 +4,7 @@ {% load humanize %} {% load admin_url %} {% load mastodon %} +{% load duration %} {% load oauth_token %} {% load truncate %} {% load highlight %} @@ -26,46 +27,59 @@ <hgroup> <h5>“{{ request.GET.q }}” {% trans '的搜索结果' %}</h5> <div> + {% visible_categories as cats %} {% if request.GET.c and request.GET.c != 'all' %} - <a href="?q={{ request.GET.q }}&c=all">全部</a> + <a href="?q={{ request.GET.q }}&c=all">全部</a> {% else %} 全部 {% endif %} - | - {% if request.GET.c != 'book' %} - <a href="?q={{ request.GET.q }}&c=book">书籍</a> - {% else %} - 书籍 + {% if 'book' in cats %} + | + {% if request.GET.c != 'book' %} + <a href="?q={{ request.GET.q }}&c=book">书籍</a> + {% else %} + 书籍 + {% endif %} {% endif %} - | - {% if request.GET.c != 'movietv' %} - <a href="?q={{ request.GET.q }}&c=movietv">影视</a> - {% else %} - 影视 + {% if 'movie' in cats or 'tv' in cats %} + | + {% if request.GET.c != 'movietv' %} + <a href="?q={{ request.GET.q }}&c=movietv">影视</a> + {% else %} + 影视 + {% endif %} {% endif %} - | - {% if request.GET.c != 'podcast' %} - <a href="?q={{ request.GET.q }}&c=podcast">播客</a> - {% else %} - 播客 + {% if 'podcast' in cats %} + | + {% if request.GET.c != 'podcast' %} + <a href="?q={{ request.GET.q }}&c=podcast">播客</a> + {% else %} + 播客 + {% endif %} {% endif %} - | - {% if request.GET.c != 'music' %} - <a href="?q={{ request.GET.q }}&c=music">音乐</a> - {% else %} - 音乐 + {% if 'music' in cats %} + | + {% if request.GET.c != 'music' %} + <a href="?q={{ request.GET.q }}&c=music">音乐</a> + {% else %} + 音乐 + {% endif %} {% endif %} - | - {% if request.GET.c != 'game' %} - <a href="?q={{ request.GET.q }}&c=game">游戏</a> - {% else %} - 游戏 + {% if 'game' in cats %} + | + {% if request.GET.c != 'game' %} + <a href="?q={{ request.GET.q }}&c=game">游戏</a> + {% else %} + 游戏 + {% endif %} {% endif %} - | - {% if request.GET.c != 'performance' %} - <a href="?q={{ request.GET.q }}&c=performance">演出</a> - {% else %} - 演出 + {% if 'performance' in cats %} + | + {% if request.GET.c != 'performance' %} + <a href="?q={{ request.GET.q }}&c=performance">演出</a> + {% else %} + 演出 + {% endif %} {% endif %} </div> </hgroup> @@ -116,24 +130,24 @@ </div> <div class="pagination"> {% if pagination.has_prev %} - <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" + <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link">«</a> - <a href="?page={{ pagination.previous_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" + <a href="?page={{ pagination.previous_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a> {% endif %} {% for page in pagination.page_range %} {% if page == pagination.current_page %} - <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" + <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a> {% else %} - <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" + <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__page-link">{{ page }}</a> {% endif %} {% endfor %} {% if pagination.has_next %} - <a href="?page={{ pagination.next_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" + <a href="?page={{ pagination.next_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">›</a> - <a href="?page={{ pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" + <a href="?page={{ pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link">»</a> {% endif %} </div> diff --git a/common/templates/_header.html b/common/templates/_header.html index 0ddaccbc..35884c1e 100644 --- a/common/templates/_header.html +++ b/common/templates/_header.html @@ -1,4 +1,5 @@ {% load admin_url %} +{% load duration %} {% load static %} {% load i18n %} <header class="container-fluid"> @@ -18,19 +19,32 @@ class="search" value="{{ request.GET.q|default:'' }}" /> <select name="c"> + {% visible_categories as cats %} <option value="all">全部</option> - <option {% if request.GET.c and request.GET.c == 'book' or '/book/' in request.path %}selected{% endif %} - value="book">书籍</option> - <option {% if request.GET.c and request.GET.c == 'movietv' or '/movie/' in request.path or '/tv/' in request.path %}selected{% endif %} - value="movietv">影视</option> - <option {% if request.GET.c and request.GET.c == 'podcast' or '/podcast/' in request.path %}selected{% endif %} - value="podcast">播客</option> - <option {% if request.GET.c and request.GET.c == 'music' or '/album/' in request.path %}selected{% endif %} - value="music">音乐</option> - <option {% if request.GET.c and request.GET.c == 'game' or '/game/' in request.path %}selected{% endif %} - value="game">游戏</option> - <option {% if request.GET.c and request.GET.c == 'performance' or '/performance/' in request.path %}selected{% endif %} - value="performance">演出</option> + {% if 'book' in cats %} + <option {% if request.GET.c == 'book' or '/book/' in request.path %}selected{% endif %} + value="book">书籍</option> + {% endif %} + {% if 'movie' in cats or 'tv' in cats %} + <option {% if request.GET.c and request.GET.c == 'movietv' or '/movie/' in request.path or '/tv/' in request.path %}selected{% endif %} + value="movietv">影视</option> + {% endif %} + {% if 'podcast' in cats %} + <option {% if request.GET.c and request.GET.c == 'podcast' or '/podcast/' in request.path %}selected{% endif %} + value="podcast">播客</option> + {% endif %} + {% if 'music' in cats %} + <option {% if request.GET.c and request.GET.c == 'music' or '/album/' in request.path %}selected{% endif %} + value="music">音乐</option> + {% endif %} + {% if 'game' in cats %} + <option {% if request.GET.c and request.GET.c == 'game' or '/game/' in request.path %}selected{% endif %} + value="game">游戏</option> + {% endif %} + {% if 'performance' in cats %} + <option {% if request.GET.c == 'performance' or '/performance/' in request.path %}selected{% endif %} + value="performance">演出</option> + {% endif %} </select> <input type="submit" value="" class="fa-solid" /> </form> diff --git a/common/templatetags/duration.py b/common/templatetags/duration.py index f095427e..90dff12e 100644 --- a/common/templatetags/duration.py +++ b/common/templatetags/duration.py @@ -2,10 +2,22 @@ from django import template from django.template.defaultfilters import stringfilter from django.utils.text import Truncator from django.utils.safestring import mark_safe +from catalog.common.models import ItemCategory, item_categories +from catalog.search.views import visible_categories as _visible_categories register = template.Library() +@register.simple_tag(takes_context=True) +def visible_categories(context): + return _visible_categories(context["request"]) + + +@register.simple_tag +def all_categories(): + return item_categories() + + @register.filter(is_safe=True) @stringfilter def duration_format(value, unit): diff --git a/doc/catalog.md b/doc/catalog.md index ec8be421..e8996226 100644 --- a/doc/catalog.md +++ b/doc/catalog.md @@ -88,7 +88,7 @@ classDiagram Add a new site -------------- -> **Site official API** should be the prioritised way to get data when adding a new site. + - If official API is available for the site, it should be the preferred way to get data. - add a new value to `IdType` and `SiteName` in `catalog/common/models.py` - add a new file in `catalog/sites/`, a new class inherits `AbstractSite`, with: * `SITE_NAME` diff --git a/users/account.py b/users/account.py index abeef85d..4da010ee 100644 --- a/users/account.py +++ b/users/account.py @@ -457,9 +457,16 @@ def swap_login(request, token, site, refresh_token): return redirect(reverse("users:data")) +def clear_preference_cache(request): + for key in list(request.session.keys()): + if key.startswith("p_"): + del request.session[key] + + def auth_login(request, user): """Decorates django ``login()``. Attach token to session.""" auth.login(request, user, backend="mastodon.auth.OAuth2Backend") + clear_preference_cache(request) if ( user.mastodon_last_refresh < timezone.now() - timedelta(hours=1) or user.mastodon_account == {} diff --git a/users/data.py b/users/data.py index 024322f0..cd046aa7 100644 --- a/users/data.py +++ b/users/data.py @@ -29,6 +29,7 @@ def preferences(request): preference.default_no_share = bool(request.POST.get("default_no_share")) preference.no_anonymous_view = bool(request.POST.get("no_anonymous_view")) preference.classic_homepage = int(request.POST.get("classic_homepage")) + preference.hidden_categories = request.POST.getlist("hidden_categories") preference.mastodon_publish_public = bool( request.POST.get("mastodon_publish_public") ) @@ -45,8 +46,10 @@ def preferences(request): "mastodon_publish_public", "mastodon_append_tag", "show_last_edit", + "hidden_categories", ] ) + clear_preference_cache(request) return render(request, "users/preferences.html") diff --git a/users/models.py b/users/models.py index cde3c8f4..9d687f06 100644 --- a/users/models.py +++ b/users/models.py @@ -20,7 +20,7 @@ from django.templatetags.static import static import hashlib from loguru import logger -RESERVED_USERNAMES = [ +_RESERVED_USERNAMES = [ "connect", "oauth2_login", "__", @@ -40,7 +40,7 @@ class UsernameValidator(validators.RegexValidator): flags = re.ASCII def __call__(self, value): - if value and value.lower() in RESERVED_USERNAMES: + if value and value.lower() in _RESERVED_USERNAMES: raise ValidationError(self.message, code=self.code) return super().__call__(value) @@ -52,6 +52,7 @@ def report_image_path(instance, filename): class User(AbstractUser): + preference: "Preference" username_validator = UsernameValidator() username = models.CharField( _("username"), @@ -457,7 +458,7 @@ class User(AbstractUser): or target.mastodon_site in self.mastodon_domain_blocks ) if target.is_authenticated - else self.preference.no_anonymous_view # type: ignore + else self.preference.no_anonymous_view ) def is_blocked_by(self, target): @@ -556,6 +557,7 @@ class Preference(models.Model): mastodon_append_tag = models.CharField(max_length=2048, default="") show_last_edit = models.PositiveSmallIntegerField(default=0) no_anonymous_view = models.PositiveSmallIntegerField(default=0) + hidden_categories = models.JSONField(default=list) def __str__(self): return str(self.user) diff --git a/users/templates/users/preferences.html b/users/templates/users/preferences.html index 1d06ee6a..01619859 100644 --- a/users/templates/users/preferences.html +++ b/users/templates/users/preferences.html @@ -4,6 +4,7 @@ {% load mastodon %} {% load oauth_token %} {% load truncate %} +{% load duration %} {% load thumb %} <!DOCTYPE html> <html lang="zh" class="classic-page"> @@ -108,6 +109,18 @@ placeholder="例如 #我的书影音" value="{{ request.user.preference.mastodon_append_tag }}"> </fieldset> + <fieldset> + <legend>{% trans '搜索时不显示以下类型:' %}</legend> + <select name="hidden_categories" size="3" multiple> + {% all_categories as categories %} + {% for c in categories %} + <option value="{{ c.value }}" + {% if c in request.user.preference.hidden_categories %}selected{% endif %}> + {{ c.label }} + </option> + {% endfor %} + </select> + </fieldset> <input type="submit" value="{% trans '保存' %}"> </form> </details>