publish tag name
[wolnelektury.git] / src / catalogue / views.py
1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 from collections import OrderedDict
5 import random
6 import re
7 from urllib.parse import quote_plus
8
9 from django.conf import settings
10 from django.template.loader import render_to_string
11 from django.shortcuts import get_object_or_404, render, redirect
12 from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect
13 from django.urls import reverse
14 from django.db.models import Q, QuerySet
15 from django.contrib.auth.decorators import login_required, user_passes_test
16 from django.utils import translation
17 from django.utils.translation import gettext as _, gettext_lazy
18 from django.views.decorators.cache import never_cache
19 from django.views.generic import TemplateView
20
21 from ajaxable.utils import AjaxableFormView
22 from club.forms import ScheduleForm, DonationStep1Form
23 from club.models import Club
24 from annoy.models import DynamicTextInsert
25 from pdcounter import views as pdcounter_views
26 from picture.models import Picture, PictureArea
27 from wolnelektury.utils import is_ajax
28 from catalogue import constants
29 from catalogue import forms
30 from catalogue.helpers import get_top_level_related_tags
31 from catalogue.models import Book, Collection, Tag, Fragment
32 from catalogue.models.tag import TagRelation
33 from catalogue.utils import split_tags
34 from catalogue.models.tag import prefetch_relations
35
36 staff_required = user_passes_test(lambda user: user.is_staff)
37
38
39 def catalogue(request):
40     return render(request, 'catalogue/catalogue.html', {
41         'books': Book.objects.filter(findable=True, parent=None),
42         'pictures': Picture.objects.all(),
43         'collections': Collection.objects.filter(listed=True),
44         'active_menu_item': 'all_works',
45     })
46
47
48 def book_list(request, filters=None, template_name='catalogue/book_list.html',
49               nav_template_name='catalogue/snippets/book_list_nav.html',
50               list_template_name='catalogue/snippets/book_list.html'):
51     """ generates a listing of all books, optionally filtered """
52     books_by_author, orphans, books_by_parent = Book.book_list(filters)
53     books_nav = OrderedDict()
54     for tag in books_by_author:
55         if books_by_author[tag]:
56             books_nav.setdefault(tag.sort_key[0], []).append(tag)
57     return render(request, template_name, {
58         'rendered_nav': render_to_string(nav_template_name, {'books_nav': books_nav}),
59         'rendered_book_list': render_to_string(list_template_name, {
60             'books_by_author': books_by_author,
61             'orphans': orphans,
62             'books_by_parent': books_by_parent,
63         })
64     })
65
66
67 def daisy_list(request):
68     if request.EXPERIMENTS['layout'].value:
69         return object_list(request, Book.objects.filter(media__type='daisy'))
70     return book_list(request, Q(media__type='daisy'), template_name='catalogue/daisy_list.html')
71
72
73 def collection(request, slug):
74     coll = get_object_or_404(Collection, slug=slug)
75     if request.EXPERIMENTS['layout'].value:
76         template_name = 'catalogue/2022/collection.html'
77     else:
78         template_name = 'catalogue/collection.html'
79     return render(request, template_name, {
80         'collection': coll,
81         'active_menu_item': 'collections',
82     })
83
84
85 def differentiate_tags(request, tags, ambiguous_slugs):
86     beginning = '/'.join(tag.url_chunk for tag in tags)
87     unparsed = '/'.join(ambiguous_slugs[1:])
88     options = []
89     for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
90         options.append({
91             'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
92             'tags': [tag]
93         })
94     return render(
95         request,
96         'catalogue/differentiate_tags.html',
97         {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
98     )
99
100
101 from django.db.models import FilteredRelation, Q
102 from django.views.decorators.cache import cache_control
103 from django.views.decorators.vary import vary_on_headers
104 from django.utils.decorators import method_decorator
105
106
107 @method_decorator([
108     vary_on_headers('X-Requested-With'),
109     cache_control(max_age=1),
110 ], 'dispatch')
111 class ObjectListView(TemplateView):
112     page_size = 100
113     themed_page_size = 10
114     item_template_name = ''
115     orderings = {}
116     default_ordering = None
117
118     def analyse(self):
119         self.is_themed = False
120         self.ctx = ctx = {}
121         ctx['tags'] = []        
122
123     def dispatch(self, *args, **kwargs):
124         try:
125             self.analyse()
126         except ResponseInstead as e:
127             return e.response
128         return super().dispatch(*args, **kwargs)
129         
130     def get_orderings(self):
131         order = self.get_order()
132         return [
133             {
134                 "slug": k,
135                 "name": v[1],
136                 "active": k == order,
137                 "default": v[0] is None,
138             }
139             for k, v in self.orderings.items()
140         ]
141
142     def get_order(self):
143         order = self.request.GET.get('order')
144         if order not in self.orderings:
145             order = self.default_ordering
146         return order
147
148     def order(self, qs):
149         order_tag = self.get_order()
150         if order_tag:
151             order = self.orderings[order_tag]
152             order_by = order[0]
153             if order_by:
154                 qs = qs.order_by(order_by)
155         return qs
156
157     def search(self, qs):
158         return qs
159
160     def get_template_names(self):
161         if is_ajax(self.request) or self.request.GET.get('dyn'):
162             if self.is_themed:
163                 return self.dynamic_themed_template_name
164             else:
165                 return self.dynamic_template_name
166         else:
167             if self.is_themed:
168                 return self.themed_template_name
169             else:
170                 return self.template_name
171
172     def get_context_data(self, **kwargs):
173         ctx = super().get_context_data()
174         ctx.update(self.ctx)
175         qs = self.get_queryset()
176         qs = self.search(qs)
177         qs = self.order(qs)
178
179         ctx['object_list'] = qs
180         ctx['suggested_tags'] = self.get_suggested_tags(qs)
181         ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
182         return ctx
183
184
185 class BookList(ObjectListView):
186     title = gettext_lazy('Literature')
187     list_type = 'books'
188     template_name = 'catalogue/2022/book_list.html'
189     dynamic_template_name = 'catalogue/2022/dynamic_book_list.html'
190     themed_template_name = 'catalogue/2022/themed_book_list.html'
191     dynamic_themed_template_name = 'catalogue/2022/dynamic_themed_book_list.html'
192
193     orderings = {
194         'pop': ('-popularity__count', 'najpopularniejsze'),
195         'alpha': (None, 'alfabetycznie'),
196     }
197     default_ordering = 'alpha'
198
199     def get_queryset(self):
200         return Book.objects.filter(parent=None, findable=True)
201
202     def search(self, qs):
203         term = self.request.GET.get('search')
204         if term:
205             meta_rels = TagRelation.objects.exclude(tag__category='set')
206             # TODO: search tags in currently displaying language
207             if self.is_themed:
208                 #qs = qs.annotate(
209                 #    meta=FilteredRelation('book__tag_relations', condition=Q(tag_relations__in=meta_rels))
210                 #)
211                 qs = qs.filter(
212                     Q(book__title__icontains=term) |
213                     #Q(meta__tag_relations__tag__name_pl__icontains=term) |
214                     Q(text__icontains=term)
215                 ).distinct()
216             else:
217                 qs = qs.annotate(
218                     meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
219                 )
220                 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
221         return qs
222
223
224 class ArtList(ObjectListView):
225     template_name = 'catalogue/2022/book_list.html'
226     dynamic_template_name = 'catalogue/2022/dynamic_book_list.html'
227     title = gettext_lazy('Art')
228     list_type = 'gallery'
229
230     def get_queryset(self):
231         return Picture.objects.all()
232
233     def search(self, qs):
234         term = self.request.GET.get('search')
235         if term:
236             qs = qs.filter(Q(title__icontains=term) | Q(tag_relations__tag__name_pl__icontains=term)).distinct()
237         return qs
238     
239
240 class LiteratureView(BookList):
241     def get_suggested_tags(self, queryset):
242         tags = list(get_top_level_related_tags([]))
243         tags.sort(key=lambda t: -t.count)
244         if self.request.user.is_authenticated:
245             tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
246         return tags
247
248
249 class AudiobooksView(LiteratureView):
250     title = gettext_lazy('Audiobooks')
251     list_type = 'audiobooks'
252
253     def get_queryset(self):
254         return Book.objects.filter(findable=True, media__type='mp3').distinct()
255
256
257 class GalleryView(ArtList):
258     def get_suggested_tags(self, queryset):
259         return Tag.objects.usage_for_queryset(
260             queryset,
261             counts=True
262         ).exclude(pk__in=[t.id for t in self.ctx['tags']]).order_by('-count')
263     
264
265 class TaggedObjectList(BookList):
266     def analyse(self):
267         super().analyse()
268         self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
269         self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
270         self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
271         self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
272         if self.is_themed:
273             self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
274         elif self.ctx['tags']:
275             self.ctx['main_tag'] = self.ctx['tags'][0]
276         else:
277             self.ctx['main_tag'] = None
278         self.ctx['filtering_tags'] = [
279             t for t in self.ctx['tags']
280             if t is not self.ctx['main_tag']
281         ]
282
283     def get_queryset(self):
284         qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
285         qs = qs.exclude(ancestor__in=qs)
286         if self.is_themed:
287             fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
288             if self.ctx['work_tags']:
289                 fqs = fqs.filter(
290                     Q(book__in=qs) | Q(book__ancestor__in=qs)
291                 )
292             qs = fqs
293         return qs
294
295     def get_suggested_tags(self, queryset):
296         tag_ids = [t.id for t in self.ctx['tags']]
297         related_tags = list(get_top_level_related_tags(self.ctx['tags']))
298         if not self.is_themed:
299             if self.request.user.is_authenticated:
300                 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
301                 related_tags.extend(list(
302                     Tag.objects.usage_for_queryset(
303                         qs
304                     ).filter(
305                         user=self.request.user
306                     ).exclude(name='').exclude(pk__in=tag_ids)
307                 ))
308
309             fragments = Fragment.objects.filter(
310                 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
311             )
312             related_tags.extend(
313                 Tag.objects.usage_for_queryset(
314                     fragments, counts=True
315                 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
316             .only('name', 'sort_key', 'category', 'slug'))
317
318         return related_tags
319
320     
321     
322 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
323                 list_type='books', extra=None):
324     if not tags:
325         tags = []
326     tag_ids = [tag.pk for tag in tags]
327
328     related_tag_lists = []
329     if related_tags:
330         related_tag_lists.append(related_tags)
331     else:
332         related_tag_lists.append(
333             Tag.objects.usage_for_queryset(
334                 objects, counts=True
335             ).exclude(category='set').exclude(pk__in=tag_ids))
336         if request.user.is_authenticated:
337             related_tag_lists.append(
338                 Tag.objects.usage_for_queryset(
339                     objects, counts=True
340                 ).filter(
341                     user=request.user
342                 ).exclude(name='').exclude(pk__in=tag_ids)
343             )
344     if not (extra and extra.get('theme_is_set')):
345         if fragments is None:
346             if list_type == 'gallery':
347                 fragments = PictureArea.objects.filter(picture__in=objects)
348             else:
349                 fragments = Fragment.objects.filter(book__in=objects)
350         related_tag_lists.append(
351             Tag.objects.usage_for_queryset(
352                 fragments, counts=True
353             ).filter(category='theme').exclude(pk__in=tag_ids)
354             .only('name', 'sort_key', 'category', 'slug'))
355         if isinstance(objects, QuerySet):
356             objects = prefetch_relations(objects, 'author')
357
358     
359     categories = split_tags(*related_tag_lists)
360     suggest = []
361     for c in ['set', 'author', 'epoch', 'kind', 'genre']:
362         suggest.extend(sorted(categories[c], key=lambda t: -t.count))
363
364     objects = list(objects)
365
366     if not objects and len(tags) == 1 and list_type == 'books':
367         if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
368             return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
369
370     if len(objects) > 3:
371         best = random.sample(objects, 3)
372     else:
373         best = objects
374
375     result = {
376         'object_list': objects,
377         'categories': categories,
378         'suggest': suggest,
379         'list_type': list_type,
380         'tags': tags,
381         'main_tag': tags[0] if tags else None,
382
383         'formats_form': forms.DownloadFormatsForm(),
384         'best': best,
385         'active_menu_item': list_type,
386     }
387     if extra:
388         result.update(extra)
389
390     if request.EXPERIMENTS['layout'].value:
391         has_theme = any(((theme := x).category == 'theme' for x in tags))
392         if has_theme:
393             result['main_tag'] = theme
394             template = 'catalogue/2022/theme_detail.html'
395         else:
396             template = 'catalogue/2022/author_detail.html'
397     else:
398         template = 'catalogue/tagged_object_list.html'
399         
400     return render(
401         request, template, result,
402     )
403
404
405 def literature(request):
406     if request.EXPERIMENTS['layout'].value:
407         return LiteratureView.as_view()(request)
408     books = Book.objects.filter(parent=None, findable=True)
409     return object_list(request, books, related_tags=get_top_level_related_tags([]))
410
411
412 def gallery(request):
413     if request.EXPERIMENTS['layout'].value:
414         return GalleryView.as_view()(request)
415     return object_list(request, Picture.objects.all(), list_type='gallery')
416
417
418 def audiobooks(request):
419     if request.EXPERIMENTS['layout'].value:
420         return AudiobooksView.as_view()(request)
421     audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
422     return object_list(request, audiobooks, list_type='audiobooks', extra={
423         'daisy': Book.objects.filter(findable=True, media__type='daisy').distinct(),
424     })
425
426
427 class ResponseInstead(Exception):
428     def __init__(self, response):
429         super(ResponseInstead, self).__init__()
430         self.response = response
431
432
433 def analyse_tags(request, tag_str):
434     try:
435         tags = Tag.get_tag_list(tag_str)
436     except Tag.DoesNotExist:
437         # Perhaps the user is asking about an author in Public Domain
438         # counter (they are not represented in tags)
439         chunks = tag_str.split('/')
440         if len(chunks) == 2 and chunks[0] == 'autor':
441             raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
442         raise Http404
443     except Tag.MultipleObjectsReturned as e:
444         # Ask the user to disambiguate
445         raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
446     except Tag.UrlDeprecationWarning as e:
447         raise ResponseInstead(HttpResponsePermanentRedirect(
448             reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
449
450     if not tags:
451         raise Http404
452     try:
453         if len(tags) > settings.MAX_TAG_LIST:
454             raise Http404
455     except AttributeError:
456         pass
457
458     return tags
459
460
461 def theme_list(request, tags, list_type):
462     shelf_tags = [tag for tag in tags if tag.category == 'set']
463     fragment_tags = [tag for tag in tags if tag.category != 'set']
464     if list_type == 'gallery':
465         fragments = PictureArea.tagged.with_all(fragment_tags)
466     else:
467         fragments = Fragment.tagged.with_all(fragment_tags)
468
469     if shelf_tags:
470         # TODO: Pictures on shelves not supported yet.
471         books = Book.tagged.with_all(shelf_tags).order_by()
472         fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
473     elif list_type == 'books':
474         fragments = fragments.filter(book__findable=True)
475
476     if not fragments and len(tags) == 1 and list_type == 'books':
477         if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
478             return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
479
480     return object_list(request, fragments, tags=tags, list_type=list_type, extra={
481         'theme_is_set': True,
482         'active_menu_item': 'theme',
483     })
484
485
486 def tagged_object_list(request, tags, list_type):
487     if request.EXPERIMENTS['layout'].value and list_type in ('books', 'audiobooks'):
488         return TaggedObjectList.as_view()(request, tags=tags)
489
490     try:
491         tags = analyse_tags(request, tags)
492     except ResponseInstead as e:
493         return e.response
494
495     if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
496         raise Http404
497
498     if any(tag.category in ('theme', 'thing') for tag in tags):
499         return theme_list(request, tags, list_type=list_type)
500
501     if list_type == 'books':
502         books = Book.tagged.with_all(tags)
503
504         if any(tag.category == 'set' for tag in tags):
505             params = {'objects': books}
506         else:
507             books = books.filter(findable=True)
508             params = {
509                 'objects': Book.tagged_top_level(tags).filter(findable=True),
510                 'fragments': Fragment.objects.filter(book__in=books),
511                 'related_tags': get_top_level_related_tags(tags),
512             }
513     elif list_type == 'gallery':
514         params = {'objects': Picture.tagged.with_all(tags)}
515     elif list_type == 'audiobooks':
516         audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
517         params = {
518             'objects': Book.tagged.with_all(tags, audiobooks),
519             'extra': {
520                 'daisy': Book.tagged.with_all(
521                     tags, audiobooks.filter(media__type='daisy').distinct()
522                 ),
523             }
524         }
525     else:
526         raise Http404
527
528     return object_list(request, tags=tags, list_type=list_type, **params)
529
530
531 def book_fragments(request, slug, theme_slug):
532     book = get_object_or_404(Book, slug=slug)
533     theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
534     fragments = Fragment.tagged.with_all([theme]).filter(
535         Q(book=book) | Q(book__ancestor=book))
536
537     if request.EXPERIMENTS['layout'].value:
538         template_name = 'catalogue/2022/book_fragments.html'
539     else:
540         template_name = 'catalogue/book_fragments.html'
541     
542     return render(
543         request,
544         template_name,
545         {
546             'book': book,
547             'theme': theme,
548             'fragments': fragments,
549             'active_menu_item': 'books',
550         })
551
552
553 @never_cache
554 def book_detail(request, slug):
555     try:
556         book = Book.objects.get(slug=slug)
557     except Book.DoesNotExist:
558         return pdcounter_views.book_stub_detail(request, slug)
559
560     new_layout = request.EXPERIMENTS['layout']
561     
562     return render(
563         request,
564         'catalogue/2022/book_detail.html' if new_layout.value else 'catalogue/book_detail.html',
565         {
566             'book': book,
567             'accessible': book.is_accessible_to(request.user),
568             'book_children': book.children.all().order_by('parent_number', 'sort_key'),
569             'active_menu_item': 'books',
570             'club_form': ScheduleForm() if book.preview else None,
571             'club': Club.objects.first() if book.preview else None,
572             'donation_form': DonationStep1Form(),
573         })
574
575
576 # używane w publicznym interfejsie
577 def player(request, slug):
578     book = get_object_or_404(Book, slug=slug)
579     if not book.has_media('mp3'):
580         raise Http404
581
582     audiobooks, projects, total_duration = book.get_audiobooks()
583
584     return render(
585         request,
586         'catalogue/player.html',
587         {
588             'book': book,
589             'audiobook': '',
590             'audiobooks': audiobooks,
591             'projects': projects,
592         })
593
594
595 def book_text(request, slug):
596     book = get_object_or_404(Book, slug=slug)
597
598     if not book.is_accessible_to(request.user):
599         return HttpResponseRedirect(book.get_absolute_url())
600
601     if not book.has_html_file():
602         raise Http404
603     with book.html_file.open('r') as f:
604         book_text = f.read()
605
606     return render(request, 'catalogue/book_text.html', {
607         'book': book,
608         'book_text': book_text,
609         'inserts': DynamicTextInsert.get_all(request)
610     })
611
612
613 # =========
614 # = Admin =
615 # =========
616 @login_required
617 @staff_required
618 def import_book(request):
619     """docstring for import_book"""
620     book_import_form = forms.BookImportForm(request.POST, request.FILES)
621     if book_import_form.is_valid():
622         try:
623             book_import_form.save()
624         except:
625             import sys
626             import pprint
627             import traceback
628             info = sys.exc_info()
629             exception = pprint.pformat(info[1])
630             tb = '\n'.join(traceback.format_tb(info[2]))
631             return HttpResponse(
632                 _("An error occurred: %(exception)s\n\n%(tb)s") % {
633                     'exception': exception, 'tb': tb
634                 },
635                 content_type='text/plain'
636             )
637         return HttpResponse(_("Book imported successfully"))
638     return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
639
640
641 # info views for API
642
643 def book_info(request, book_id, lang='pl'):
644     book = get_object_or_404(Book, id=book_id)
645     # set language by hand
646     translation.activate(lang)
647     return render(request, 'catalogue/book_info.html', {'book': book})
648
649
650 def tag_info(request, tag_id):
651     tag = get_object_or_404(Tag, id=tag_id)
652     return HttpResponse(tag.description)
653
654
655 @never_cache
656 def embargo_link(request, key, format_, slug):
657     book = get_object_or_404(Book, slug=slug)
658     if format_ not in Book.formats:
659         raise Http404
660     if key != book.preview_key:
661         raise Http404
662     media_file = book.get_media(format_)
663     if not book.preview:
664         return HttpResponseRedirect(media_file.url)
665     return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
666
667
668 def download_zip(request, file_format=None, media_format=None, slug=None):
669     if file_format:
670         url = Book.zip_format(file_format)
671     elif media_format and slug is not None:
672         book = get_object_or_404(Book, slug=slug)
673         url = book.zip_audiobooks(media_format)
674     else:
675         raise Http404('No format specified for zip package')
676     return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
677
678
679 class CustomPDFFormView(AjaxableFormView):
680     form_class = forms.CustomPDFForm
681     title = gettext_lazy('Download custom PDF')
682     submit = gettext_lazy('Download')
683     template = 'catalogue/custom_pdf_form.html'
684     honeypot = True
685
686     def __call__(self, *args, **kwargs):
687         if settings.NO_CUSTOM_PDF:
688             raise Http404('Custom PDF is disabled')
689         return super(CustomPDFFormView, self).__call__(*args, **kwargs)
690
691     def form_args(self, request, obj):
692         """Override to parse view args and give additional args to the form."""
693         return (obj,), {}
694
695     def validate_object(self, obj, request):
696         book = obj
697         if not book.is_accessible_to(request.user):
698             return HttpResponseRedirect(book.get_absolute_url())
699         return super(CustomPDFFormView, self).validate_object(obj, request)
700
701     def get_object(self, request, slug, *args, **kwargs):
702         book = get_object_or_404(Book, slug=slug)
703         return book
704
705     def context_description(self, request, obj):
706         return obj.pretty_title()
707
708
709 def tag_catalogue(request, category):
710     if category == 'theme':
711         tags = Tag.objects.usage_for_model(
712             Fragment, counts=True).filter(category='theme')
713     else:
714         tags = list(get_top_level_related_tags((), categories=(category,)))
715
716     described_tags = [tag for tag in tags if tag.description]
717
718     if len(described_tags) > 4:
719         best = random.sample(described_tags, 4)
720     else:
721         best = described_tags
722
723     if request.EXPERIMENTS['layout'].value:
724         template_name = 'catalogue/2022/tag_catalogue.html'
725     else:
726         template_name = 'catalogue/tag_catalogue.html'
727
728     return render(request, template_name, {
729         'tags': tags,
730         'best': best,
731         'title': constants.CATEGORIES_NAME_PLURAL[category],
732         'whole_category': constants.WHOLE_CATEGORY[category],
733         'active_menu_item': 'theme' if category == 'theme' else None,
734     })
735
736
737 def collections(request):
738     objects = Collection.objects.filter(listed=True)
739
740     if len(objects) > 3:
741         best = random.sample(list(objects), 4)
742     else:
743         best = objects
744
745     if request.EXPERIMENTS['layout'].value:
746         template_name = 'catalogue/2022/collections.html'
747     else:
748         template_name = 'catalogue/collections.html'
749
750     return render(request, template_name, {
751         'objects': objects,
752         'best': best,
753         'active_menu_item': 'collections'
754     })
755
756
757 def ridero_cover(request, slug):
758     from librarian.cover import make_cover
759     wldoc = Book.objects.get(slug=slug).wldocument()
760     cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
761     response = HttpResponse(content_type="image/png")
762     cover.save(response)
763     return response
764
765
766 def get_isbn(request, book_format, slug):
767     book = Book.objects.get(slug=slug)
768     return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))