From ea9d7d13a44a6ad6357b1af8c977c27753268521 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 9 Jun 2023 15:18:24 +0200 Subject: [PATCH] Dynamic object lists. --- src/catalogue/static/2022/book/filter.js | 91 ++++++-- .../templates/catalogue/2022/book_list.html | 126 +++++++++++ .../catalogue/2022/dynamic_book_list.html | 14 ++ .../2022/dynamic_themed_book_list.html | 14 ++ .../catalogue/2022/themed_book_list.html | 90 ++++++++ src/catalogue/urls.py | 8 +- src/catalogue/views.py | 211 +++++++++++++++++- src/experiments/experiments.py | 11 - src/search/templates/search/results.html | 54 +++-- src/search/views.py | 3 +- src/wolnelektury/static/2021/scripts/main.js | 4 +- .../2022/styles/layout/_pagination.scss | 4 + 12 files changed, 575 insertions(+), 55 deletions(-) create mode 100644 src/catalogue/templates/catalogue/2022/book_list.html create mode 100644 src/catalogue/templates/catalogue/2022/dynamic_book_list.html create mode 100644 src/catalogue/templates/catalogue/2022/dynamic_themed_book_list.html create mode 100644 src/catalogue/templates/catalogue/2022/themed_book_list.html diff --git a/src/catalogue/static/2022/book/filter.js b/src/catalogue/static/2022/book/filter.js index bdf6357ef..59e810a48 100644 --- a/src/catalogue/static/2022/book/filter.js +++ b/src/catalogue/static/2022/book/filter.js @@ -1,5 +1,43 @@ (function($) { + let unpagedSearch = null; + if (!$(".quick-filter").val() && !$('.l-pagination li').length) { + unpagedSearch = ''; + } + + function get_page(page, search, ordering, callback) { + get_page_by_url('.?page=' + page + '&order=' + ordering + '&search=' + search, callback); + } + function get_page_by_url(url, callback) { + $.get( + url, + function(data) { + html = $(data); + objectList = $('#object-list', html); + paginate = $('#paginate', html); + + ids = new Set(); + $(".icon-like", objectList).each( + (i, e)=>{ + ids.add($(e).attr('data-book')); + } + ); + ids = [...ids].join(','); + $.refreshLikes(ids); + + $('#book-list').html(objectList.children()); + $('#paginator').html(paginate.children()); + history.replaceState({}, '', url); + callback && callback(); + } + ) + } + + $("#paginator").on('click', 'a', function() { + get_page_by_url(url=$(this).attr('href')); + return false; + }); + $(".quick-filter").each(function() { let bookList = $('#' + $(this).data('for')); let filterList = $('.' + $(this).data('filters')); @@ -14,20 +52,25 @@ $(this).on('input propertychange', function() { let search = $(this).val().toLowerCase(); - bookList.children().each(function() { - found = !search || - $(".s", this).text().toLowerCase().search(search) != -1 + if (!search.startsWith(unpagedSearch)) { + get_page(1, search, 'title', function() { + if ($('.l-pagination li').length) { + unpagedSearch = null; + } + }) + } else { + bookList.children().each(function() { + found = !search || + $(".s", this).text().toLowerCase().search(search) != -1 ; - if (found) - $(this).fadeIn(); - else - $(this).fadeOut(); - }); - - + if (found) + $(this).fadeIn(); + else + $(this).fadeOut(); + }); + } $('.filter-container', filterList).children().each(function() { - console.log($(this).text().toLowerCase()); found = !search || $(this).text().toLowerCase().search(search) != -1 ; @@ -45,18 +88,28 @@ $(".is-active", $(this).parent()).removeClass("is-active"); $(this).addClass("is-active"); let prop = $(this).attr('data-order'); + + + // do we NOW have pages (possibly after filtering)? + // if we don't have pages, we can just sort here. + let havePages = $('.l-pagination li').length > 0; + $(".l-books__item").css('opacity', '0'); setTimeout(function() { - if (prop) { - $(".l-books__item").each(function() { - $(this).css('order', $(this).attr(prop)); - }); + if (havePages) { + get_page(1, '', prop); } else { - $(".l-books__item").css('order', ''); + if (prop) { + $(".l-books__item").each(function() { + $(this).css('order', $(this).attr('data-' + prop)); + }); + } else { + $(".l-books__item").css('order', ''); + } + setTimeout(function() { + $(".l-books__item").css('opacity', '100%'); + }, 200); } - setTimeout(function() { - $(".l-books__item").css('opacity', '100%'); - }, 200); }, 200); }); diff --git a/src/catalogue/templates/catalogue/2022/book_list.html b/src/catalogue/templates/catalogue/2022/book_list.html new file mode 100644 index 000000000..e08cf3ad9 --- /dev/null +++ b/src/catalogue/templates/catalogue/2022/book_list.html @@ -0,0 +1,126 @@ +{% extends '2022/base.html' %} +{% load catalogue_tags %} +{% load thumbnail %} +{% load pagination_tags %} + +{% load choose_cites from social_tags %} + + +{% block breadcrumbs %} + Katalog + {% if main_tag %} + {{ main_tag.get_category_display|title }} + {% endif %} +{% endblock %} + +{% block main %} +
+
+ {% if main_tag.photo %} + {% thumbnail main_tag.photo '40x40' crop='center' as th %} +
+ {{ main_tag.name }} +
+ {% endthumbnail %} + {% endif %} +

+ {% if main_tag %} + {{ main_tag.name }} + {% else %} + {{ view.title }} + {% endif %} +

+
+
+ +
+
+
+ + +
+ {% for tag in tags %} + {% if tag is not main_tag %} + + {{ tag }} + ✖ + + {% endif %} + {% endfor %} +
+ +
+ {% if view.get_orderings %} +
+ Sortuj: +
+ {% for ordering in view.get_orderings %} + + {% endfor %} +
+
+ {% endif %} +
+
+
+
+

{% nice_title_from_tags tags suggested_tags_by_category %}

+ {% if suggested_tags %} +
+ {% for tag in suggested_tags %} + + {{ tag }} + + {% endfor %} +
+ {% endif %} +
+
+ + {% autopaginate object_list view.page_size %} + +
+
+ {% for book in object_list %} + {% include "catalogue/2022/book_box.html" %} + {% endfor %} +
+
+ +
+ {% paginate using '2022/paginate.html' %} +
+ + {% if main_tag %} +
+
+ {% with tag=main_tag %} + {% include 'catalogue/2022/author_box.html' %} + {% endwith %} + {% choose_cites 3 author=main_tag as cites %} + {% if cites %} +
+
+
+ + {% for fragment in cites %} +
+ {% include "catalogue/2022/fragment_slider_box.html" %} +
+ {% endfor %} + +
+
+
+ {% endif %} +
+
+ {% endif %} +{% endblock %} diff --git a/src/catalogue/templates/catalogue/2022/dynamic_book_list.html b/src/catalogue/templates/catalogue/2022/dynamic_book_list.html new file mode 100644 index 000000000..0511be5da --- /dev/null +++ b/src/catalogue/templates/catalogue/2022/dynamic_book_list.html @@ -0,0 +1,14 @@ +{% load pagination_tags %} +{% autopaginate object_list view.page_size %} + +
+
+ {% for book in object_list %} + {% include "catalogue/2022/book_box.html" %} + {% endfor %} +
+ +
+ {% paginate using '2022/paginate.html' %} +
+
diff --git a/src/catalogue/templates/catalogue/2022/dynamic_themed_book_list.html b/src/catalogue/templates/catalogue/2022/dynamic_themed_book_list.html new file mode 100644 index 000000000..3346d8885 --- /dev/null +++ b/src/catalogue/templates/catalogue/2022/dynamic_themed_book_list.html @@ -0,0 +1,14 @@ +{% load pagination_tags %} +{% autopaginate object_list view.themed_page_size %} + +
+
+ {% for fragment in object_list %} + {% include "catalogue/2022/fragment_box.html" %} + {% endfor %} +
+ +
+ {% paginate using '2022/paginate.html' %} +
+
diff --git a/src/catalogue/templates/catalogue/2022/themed_book_list.html b/src/catalogue/templates/catalogue/2022/themed_book_list.html new file mode 100644 index 000000000..8ba5e5861 --- /dev/null +++ b/src/catalogue/templates/catalogue/2022/themed_book_list.html @@ -0,0 +1,90 @@ +{% extends '2022/base.html' %} +{% load catalogue_tags %} +{% load pagination_tags %} + +{% block global-content %} +
+ +
+ +
+ +
+
+

Motyw: {{ main_tag.name }}

+
+
+ + +
+
+
+ + +
+ {% for tag in tags %} + {% if tag is not main_tag %} + + {{ tag }} + ✖ + + {% endif %} + {% endfor %} +
+ +
+
+
+
+
+

{% nice_title_from_tags tags categories %}

+ {% if suggest %} +
+ {% for tag in suggest %} + + {{ tag }} + + {% endfor %} +
+ {% endif %} +
+
+ + {% autopaginate object_list view.themed_page_size %} + +
+
+
+
+ {% for fragment in object_list %} + {% include 'catalogue/2022/fragment_box.html' %} + {% endfor %} +
+
+ {% paginate using '2022/paginate.html' %} +
+
+
+
+

Motyw: {{ main_tag.name }}

+ {{ main_tag.description|safe }} + +
+
+
+
+
+ +{% endblock %} diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 7049b1c85..840d2bfae 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -30,12 +30,12 @@ urlpatterns = [ path('rodzaj/', views.tag_catalogue, {'category': 'kind'}, name='kind_catalogue'), path('motyw/', views.tag_catalogue, {'category': 'theme'}, name='theme_catalogue'), - path('galeria/', views.gallery, name='gallery'), + path('galeria/', views.GalleryView.as_view(), name='gallery'), path('kolekcje/', views.collections, name='catalogue_collections'), - path('lektury/', views.literature, name='book_list'), + path('lektury/', views.LiteratureView.as_view(), name='book_list'), path('lektury//', views.collection, name='collection'), - path('audiobooki/', views.audiobooks, name='audiobook_list'), + path('audiobooki/', views.AudiobooksView.as_view(), name='audiobook_list'), path('daisy/', views.daisy_list, name='daisy_list'), path('jtags/', search.views.hint, {'param': 'q', 'mozhint': True}, name='jhint'), path('nowe/', ListView.as_view( @@ -76,7 +76,7 @@ urlpatterns = [ name='tagged_object_list_gallery'), re_path(r'^audiobooki/(?P[a-zA-Z0-9-/]*)/$', views.tagged_object_list, {'list_type': 'audiobooks'}, name='tagged_object_list_audiobooks'), - re_path(r'^(?P[a-zA-Z0-9-/]*)/$', views.tagged_object_list, {'list_type': 'books'}, + re_path(r'^(?P[a-zA-Z0-9-/]*)/$', views.TaggedObjectList.as_view(), name='tagged_object_list'), ] diff --git a/src/catalogue/views.py b/src/catalogue/views.py index e02720010..2d8aea715 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -16,6 +16,7 @@ from django.contrib.auth.decorators import login_required, user_passes_test from django.utils import translation from django.utils.translation import gettext as _, gettext_lazy from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView from ajaxable.utils import AjaxableFormView from club.forms import ScheduleForm, DonationStep1Form @@ -23,10 +24,12 @@ from club.models import Club from annoy.models import DynamicTextInsert from pdcounter import views as pdcounter_views from picture.models import Picture, PictureArea +from wolnelektury.utils import is_ajax from catalogue import constants from catalogue import forms from catalogue.helpers import get_top_level_related_tags from catalogue.models import Book, Collection, Tag, Fragment +from catalogue.models.tag import TagRelation from catalogue.utils import split_tags from catalogue.models.tag import prefetch_relations @@ -95,6 +98,212 @@ def differentiate_tags(request, tags, ambiguous_slugs): ) +from django.db.models import FilteredRelation, Q +from django.views.decorators.cache import cache_control +from django.views.decorators.vary import vary_on_headers +from django.utils.decorators import method_decorator + + +@method_decorator([ + vary_on_headers('X-Requested-With'), + cache_control(max_age=1), +], 'dispatch') +class ObjectListView(TemplateView): + page_size = 100 + themed_page_size = 10 + item_template_name = '' + orderings = {} + default_ordering = None + + def setup(self, request, **kwargs): + super().setup(request, **kwargs) + self.is_themed = False + self.ctx = ctx = {} + ctx['tags'] = [] + + def get_orderings(self): + order = self.get_order() + return [ + { + "slug": k, + "name": v[1], + "active": k == order, + "default": v[0] is None, + } + for k, v in self.orderings.items() + ] + + def get_order(self): + order = self.request.GET.get('order') + if order not in self.orderings: + order = self.default_ordering + return order + + def order(self, qs): + order_tag = self.get_order() + if order_tag: + order = self.orderings[order_tag] + order_by = order[0] + if order_by: + qs = qs.order_by(order_by) + return qs + + def search(self, qs): + return qs + + def get_template_names(self): + if is_ajax(self.request) or self.request.GET.get('dyn'): + if self.is_themed: + return self.dynamic_themed_template_name + else: + return self.dynamic_template_name + else: + if self.is_themed: + return self.themed_template_name + else: + return self.template_name + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + ctx.update(self.ctx) + qs = self.get_queryset() + qs = self.search(qs) + qs = self.order(qs) + + ctx['object_list'] = qs + ctx['suggested_tags'] = self.get_suggested_tags(qs) + ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags']) + return ctx + + +class BookList(ObjectListView): + title = gettext_lazy('Literature') + list_type = 'books' + template_name = 'catalogue/2022/book_list.html' + dynamic_template_name = 'catalogue/2022/dynamic_book_list.html' + themed_template_name = 'catalogue/2022/themed_book_list.html' + dynamic_themed_template_name = 'catalogue/2022/dynamic_themed_book_list.html' + + orderings = { + 'pop': ('-popularity__count', 'najpopularniejsze'), + 'alpha': (None, 'alfabetycznie'), + } + default_ordering = 'alpha' + + def get_queryset(self): + return Book.objects.filter(parent=None, findable=True) + + def search(self, qs): + term = self.request.GET.get('search') + if term: + meta_rels = TagRelation.objects.exclude(tag__category='set') + # TODO: search tags in currently displaying language + if self.is_themed: + #qs = qs.annotate( + # meta=FilteredRelation('book__tag_relations', condition=Q(tag_relations__in=meta_rels)) + #) + qs = qs.filter( + Q(book__title__icontains=term) | + #Q(meta__tag_relations__tag__name_pl__icontains=term) | + Q(text__icontains=term) + ).distinct() + else: + qs = qs.annotate( + meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels)) + ) + qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct() + return qs + + +class ArtList(ObjectListView): + template_name = 'catalogue/2022/book_list.html' + dynamic_template_name = 'catalogue/2022/dynamic_book_list.html' + title = gettext_lazy('Art') + list_type = 'gallery' + + def get_queryset(self): + return Picture.objects.all() + + def search(self, qs): + term = self.request.GET.get('search') + if term: + qs = qs.filter(Q(title__icontains=term) | Q(tag_relations__tag__name_pl__icontains=term)).distinct() + return qs + + +class LiteratureView(BookList): + def get_suggested_tags(self, queryset): + tags = list(get_top_level_related_tags([])) + tags.sort(key=lambda t: -t.count) + if self.request.user.is_authenticated: + tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name=''))) + return tags + + +class AudiobooksView(LiteratureView): + title = gettext_lazy('Audiobooks') + + def get_queryset(self): + return Book.objects.filter(findable=True, media__type='mp3').distinct() + + +class GalleryView(ArtList): + def get_suggested_tags(self, queryset): + return Tag.objects.usage_for_queryset( + queryset, + counts=True + ).exclude(pk__in=[t.id for t in self.ctx['tags']]).order_by('-count') + + +class TaggedObjectList(BookList): + def setup(self, request, tags, **kwargs): + super().setup(request, **kwargs) + self.ctx['tags'] = analyse_tags(request, tags) + self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')] + self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']] + self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags']) + self.ctx['main_tag'] = self.ctx['fragment_tags'][0] if self.is_themed else self.ctx['tags'][0] + self.ctx['filtering_tags'] = [ + t for t in self.ctx['tags'] + if t is not self.ctx['main_tag'] + ] + + def get_queryset(self): + qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True) + qs = qs.exclude(ancestor__in=qs) + if self.is_themed: + qs = Fragment.tagged.with_all(self.ctx['fragment_tags']).filter( + Q(book__in=qs) | Q(book__ancestor__in=qs) + ) + return qs + + def get_suggested_tags(self, queryset): + tag_ids = [t.id for t in self.ctx['tags']] + related_tags = list(get_top_level_related_tags(self.ctx['tags'])) + if not self.is_themed: + if self.request.user.is_authenticated: + qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True) + related_tags.extend(list( + Tag.objects.usage_for_queryset( + qs + ).filter( + user=self.request.user + ).exclude(name='').exclude(pk__in=tag_ids) + )) + + fragments = Fragment.objects.filter( + Q(book__in=queryset) | Q(book__ancestor__in=queryset) + ) + related_tags.extend( + Tag.objects.usage_for_queryset( + fragments, counts=True + ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids) + .only('name', 'sort_key', 'category', 'slug')) + + return related_tags + + + def object_list(request, objects, fragments=None, related_tags=None, tags=None, list_type='books', extra=None): if not tags: @@ -135,7 +344,7 @@ def object_list(request, objects, fragments=None, related_tags=None, tags=None, categories = split_tags(*related_tag_lists) suggest = [] for c in ['set', 'author', 'epoch', 'kind', 'genre']: - suggest.extend(sorted(categories[c], key=lambda t: -t.count)[:3]) + suggest.extend(sorted(categories[c], key=lambda t: -t.count)) objects = list(objects) diff --git a/src/experiments/experiments.py b/src/experiments/experiments.py index 60d05213d..d38666d74 100644 --- a/src/experiments/experiments.py +++ b/src/experiments/experiments.py @@ -9,10 +9,6 @@ class NewLayout(Experiment): name = 'Nowy layout strony' size = settings.EXPERIMENTS_LAYOUT - def qualify(self, request): - if get_language() != 'pl': - return False - class Sowka(Experiment): slug = 'sowka' @@ -21,14 +17,7 @@ class Sowka(Experiment): switchable = False -class Search(Experiment): - slug = 'search' - name = 'Nowa wyszukiwarka' - size = settings.EXPERIMENTS_SEARCH - - experiments = [ NewLayout, Sowka, - Search ] diff --git a/src/search/templates/search/results.html b/src/search/templates/search/results.html index dbd0d6ffd..625bb6591 100644 --- a/src/search/templates/search/results.html +++ b/src/search/templates/search/results.html @@ -42,23 +42,23 @@ {% endif %} {% if results.author %} -
-

Autorzy

- -
+
+

Autorzy

+ +
{% endif %} {% if results.theme %} @@ -81,6 +81,26 @@ {% endif %} + {% if results.genre %} +
+

Gatunki

+ +
+ {% endif %} + {% if results.book %}

Książki

diff --git a/src/search/views.py b/src/search/views.py index 6ff0f7ab6..9e3645858 100644 --- a/src/search/views.py +++ b/src/search/views.py @@ -137,8 +137,7 @@ def search(request): @cache.never_cache def main(request): - if request.EXPERIMENTS['search'].value: - request.EXPERIMENTS['layout'].override(True) + if request.EXPERIMENTS['layout'].value: return search(request) query = request.GET.get('q', '') diff --git a/src/wolnelektury/static/2021/scripts/main.js b/src/wolnelektury/static/2021/scripts/main.js index 843cddbbf..3bbb38d55 100644 --- a/src/wolnelektury/static/2021/scripts/main.js +++ b/src/wolnelektury/static/2021/scripts/main.js @@ -401,7 +401,7 @@ liked: [], }; - $(".icon-like").on('click', function(e) { + $(document).on('click', '.icon-like', function(e) { e.preventDefault(); let liked = $(this).hasClass('icon-liked'); $btn = $(this); @@ -431,6 +431,7 @@ } }) + // TODO: DYNAMICALLY ADD $(".add-set-tag input[name=name]").autocomplete({ source: '/ludzie/moje-tagi/', }).on('autocompleteopen', function() { @@ -478,6 +479,7 @@ }); } refreshAll(ids); + $.refreshLikes = refreshAll; function updateFromData(data) { for (pk in data) { diff --git a/src/wolnelektury/static/2022/styles/layout/_pagination.scss b/src/wolnelektury/static/2022/styles/layout/_pagination.scss index d24367079..ec8ace776 100644 --- a/src/wolnelektury/static/2022/styles/layout/_pagination.scss +++ b/src/wolnelektury/static/2022/styles/layout/_pagination.scss @@ -1,3 +1,7 @@ +#paginator { + display: flex; +} + .l-pagination { display: flex; align-content: center; -- 2.20.1