d563b68a647a7b8db8c35bc3f7db59d38ce3c031
[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             qs = Fragment.tagged.with_all(self.ctx['fragment_tags']).filter(
288                 Q(book__in=qs) | Q(book__ancestor__in=qs)
289             )
290         return qs
291
292     def get_suggested_tags(self, queryset):
293         tag_ids = [t.id for t in self.ctx['tags']]
294         related_tags = list(get_top_level_related_tags(self.ctx['tags']))
295         if not self.is_themed:
296             if self.request.user.is_authenticated:
297                 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
298                 related_tags.extend(list(
299                     Tag.objects.usage_for_queryset(
300                         qs
301                     ).filter(
302                         user=self.request.user
303                     ).exclude(name='').exclude(pk__in=tag_ids)
304                 ))
305
306             fragments = Fragment.objects.filter(
307                 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
308             )
309             related_tags.extend(
310                 Tag.objects.usage_for_queryset(
311                     fragments, counts=True
312                 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
313             .only('name', 'sort_key', 'category', 'slug'))
314
315         return related_tags
316
317     
318     
319 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
320                 list_type='books', extra=None):
321     if not tags:
322         tags = []
323     tag_ids = [tag.pk for tag in tags]
324
325     related_tag_lists = []
326     if related_tags:
327         related_tag_lists.append(related_tags)
328     else:
329         related_tag_lists.append(
330             Tag.objects.usage_for_queryset(
331                 objects, counts=True
332             ).exclude(category='set').exclude(pk__in=tag_ids))
333         if request.user.is_authenticated:
334             related_tag_lists.append(
335                 Tag.objects.usage_for_queryset(
336                     objects, counts=True
337                 ).filter(
338                     user=request.user
339                 ).exclude(name='').exclude(pk__in=tag_ids)
340             )
341     if not (extra and extra.get('theme_is_set')):
342         if fragments is None:
343             if list_type == 'gallery':
344                 fragments = PictureArea.objects.filter(picture__in=objects)
345             else:
346                 fragments = Fragment.objects.filter(book__in=objects)
347         related_tag_lists.append(
348             Tag.objects.usage_for_queryset(
349                 fragments, counts=True
350             ).filter(category='theme').exclude(pk__in=tag_ids)
351             .only('name', 'sort_key', 'category', 'slug'))
352         if isinstance(objects, QuerySet):
353             objects = prefetch_relations(objects, 'author')
354
355     
356     categories = split_tags(*related_tag_lists)
357     suggest = []
358     for c in ['set', 'author', 'epoch', 'kind', 'genre']:
359         suggest.extend(sorted(categories[c], key=lambda t: -t.count))
360
361     objects = list(objects)
362
363     if not objects and len(tags) == 1 and list_type == 'books':
364         if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
365             return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
366
367     if len(objects) > 3:
368         best = random.sample(objects, 3)
369     else:
370         best = objects
371
372     result = {
373         'object_list': objects,
374         'categories': categories,
375         'suggest': suggest,
376         'list_type': list_type,
377         'tags': tags,
378         'main_tag': tags[0] if tags else None,
379
380         'formats_form': forms.DownloadFormatsForm(),
381         'best': best,
382         'active_menu_item': list_type,
383     }
384     if extra:
385         result.update(extra)
386
387     if request.EXPERIMENTS['layout'].value:
388         has_theme = any(((theme := x).category == 'theme' for x in tags))
389         if has_theme:
390             result['main_tag'] = theme
391             template = 'catalogue/2022/theme_detail.html'
392         else:
393             template = 'catalogue/2022/author_detail.html'
394     else:
395         template = 'catalogue/tagged_object_list.html'
396         
397     return render(
398         request, template, result,
399     )
400
401
402 def literature(request):
403     if request.EXPERIMENTS['layout'].value:
404         return LiteratureView.as_view()(request)
405     books = Book.objects.filter(parent=None, findable=True)
406     return object_list(request, books, related_tags=get_top_level_related_tags([]))
407
408
409 def gallery(request):
410     if request.EXPERIMENTS['layout'].value:
411         return GalleryView.as_view()(request)
412     return object_list(request, Picture.objects.all(), list_type='gallery')
413
414
415 def audiobooks(request):
416     if request.EXPERIMENTS['layout'].value:
417         return AudiobooksView.as_view()(request)
418     audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
419     return object_list(request, audiobooks, list_type='audiobooks', extra={
420         'daisy': Book.objects.filter(findable=True, media__type='daisy').distinct(),
421     })
422
423
424 class ResponseInstead(Exception):
425     def __init__(self, response):
426         super(ResponseInstead, self).__init__()
427         self.response = response
428
429
430 def analyse_tags(request, tag_str):
431     try:
432         tags = Tag.get_tag_list(tag_str)
433     except Tag.DoesNotExist:
434         # Perhaps the user is asking about an author in Public Domain
435         # counter (they are not represented in tags)
436         chunks = tag_str.split('/')
437         if len(chunks) == 2 and chunks[0] == 'autor':
438             raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
439         raise Http404
440     except Tag.MultipleObjectsReturned as e:
441         # Ask the user to disambiguate
442         raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
443     except Tag.UrlDeprecationWarning as e:
444         raise ResponseInstead(HttpResponsePermanentRedirect(
445             reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
446
447     if not tags:
448         raise Http404
449     try:
450         if len(tags) > settings.MAX_TAG_LIST:
451             raise Http404
452     except AttributeError:
453         pass
454
455     return tags
456
457
458 def theme_list(request, tags, list_type):
459     shelf_tags = [tag for tag in tags if tag.category == 'set']
460     fragment_tags = [tag for tag in tags if tag.category != 'set']
461     if list_type == 'gallery':
462         fragments = PictureArea.tagged.with_all(fragment_tags)
463     else:
464         fragments = Fragment.tagged.with_all(fragment_tags)
465
466     if shelf_tags:
467         # TODO: Pictures on shelves not supported yet.
468         books = Book.tagged.with_all(shelf_tags).order_by()
469         fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
470     elif list_type == 'books':
471         fragments = fragments.filter(book__findable=True)
472
473     if not fragments and len(tags) == 1 and list_type == 'books':
474         if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
475             return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
476
477     return object_list(request, fragments, tags=tags, list_type=list_type, extra={
478         'theme_is_set': True,
479         'active_menu_item': 'theme',
480     })
481
482
483 def tagged_object_list(request, tags, list_type):
484     if request.EXPERIMENTS['layout'].value and list_type in ('books', 'audiobooks'):
485         return TaggedObjectList.as_view()(request, tags=tags)
486
487     try:
488         tags = analyse_tags(request, tags)
489     except ResponseInstead as e:
490         return e.response
491
492     if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
493         raise Http404
494
495     if any(tag.category in ('theme', 'thing') for tag in tags):
496         return theme_list(request, tags, list_type=list_type)
497
498     if list_type == 'books':
499         books = Book.tagged.with_all(tags)
500
501         if any(tag.category == 'set' for tag in tags):
502             params = {'objects': books}
503         else:
504             books = books.filter(findable=True)
505             params = {
506                 'objects': Book.tagged_top_level(tags).filter(findable=True),
507                 'fragments': Fragment.objects.filter(book__in=books),
508                 'related_tags': get_top_level_related_tags(tags),
509             }
510     elif list_type == 'gallery':
511         params = {'objects': Picture.tagged.with_all(tags)}
512     elif list_type == 'audiobooks':
513         audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
514         params = {
515             'objects': Book.tagged.with_all(tags, audiobooks),
516             'extra': {
517                 'daisy': Book.tagged.with_all(
518                     tags, audiobooks.filter(media__type='daisy').distinct()
519                 ),
520             }
521         }
522     else:
523         raise Http404
524
525     return object_list(request, tags=tags, list_type=list_type, **params)
526
527
528 def book_fragments(request, slug, theme_slug):
529     book = get_object_or_404(Book, slug=slug)
530     theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
531     fragments = Fragment.tagged.with_all([theme]).filter(
532         Q(book=book) | Q(book__ancestor=book))
533
534     if request.EXPERIMENTS['layout'].value:
535         template_name = 'catalogue/2022/book_fragments.html'
536     else:
537         template_name = 'catalogue/book_fragments.html'
538     
539     return render(
540         request,
541         template_name,
542         {
543             'book': book,
544             'theme': theme,
545             'fragments': fragments,
546             'active_menu_item': 'books',
547         })
548
549
550 @never_cache
551 def book_detail(request, slug):
552     try:
553         book = Book.objects.get(slug=slug)
554     except Book.DoesNotExist:
555         return pdcounter_views.book_stub_detail(request, slug)
556
557     new_layout = request.EXPERIMENTS['layout']
558     
559     return render(
560         request,
561         'catalogue/2022/book_detail.html' if new_layout.value else 'catalogue/book_detail.html',
562         {
563             'book': book,
564             'accessible': book.is_accessible_to(request.user),
565             'book_children': book.children.all().order_by('parent_number', 'sort_key'),
566             'active_menu_item': 'books',
567             'club_form': ScheduleForm() if book.preview else None,
568             'club': Club.objects.first() if book.preview else None,
569             'donation_form': DonationStep1Form(),
570
571             'EXPERIMENTS_SWITCHABLE_layout': True,
572         })
573
574
575 # używane w publicznym interfejsie
576 def player(request, slug):
577     book = get_object_or_404(Book, slug=slug)
578     if not book.has_media('mp3'):
579         raise Http404
580
581     audiobooks, projects, total_duration = book.get_audiobooks()
582
583     return render(
584         request,
585         'catalogue/player.html',
586         {
587             'book': book,
588             'audiobook': '',
589             'audiobooks': audiobooks,
590             'projects': projects,
591         })
592
593
594 def book_text(request, slug):
595     book = get_object_or_404(Book, slug=slug)
596
597     if not book.is_accessible_to(request.user):
598         return HttpResponseRedirect(book.get_absolute_url())
599
600     if not book.has_html_file():
601         raise Http404
602     with book.html_file.open('r') as f:
603         book_text = f.read()
604
605     return render(request, 'catalogue/book_text.html', {
606         'book': book,
607         'book_text': book_text,
608         'inserts': DynamicTextInsert.get_all(request)
609     })
610
611
612 # =========
613 # = Admin =
614 # =========
615 @login_required
616 @staff_required
617 def import_book(request):
618     """docstring for import_book"""
619     book_import_form = forms.BookImportForm(request.POST, request.FILES)
620     if book_import_form.is_valid():
621         try:
622             book_import_form.save()
623         except:
624             import sys
625             import pprint
626             import traceback
627             info = sys.exc_info()
628             exception = pprint.pformat(info[1])
629             tb = '\n'.join(traceback.format_tb(info[2]))
630             return HttpResponse(
631                 _("An error occurred: %(exception)s\n\n%(tb)s") % {
632                     'exception': exception, 'tb': tb
633                 },
634                 content_type='text/plain'
635             )
636         return HttpResponse(_("Book imported successfully"))
637     return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
638
639
640 # info views for API
641
642 def book_info(request, book_id, lang='pl'):
643     book = get_object_or_404(Book, id=book_id)
644     # set language by hand
645     translation.activate(lang)
646     return render(request, 'catalogue/book_info.html', {'book': book})
647
648
649 def tag_info(request, tag_id):
650     tag = get_object_or_404(Tag, id=tag_id)
651     return HttpResponse(tag.description)
652
653
654 @never_cache
655 def embargo_link(request, key, format_, slug):
656     book = get_object_or_404(Book, slug=slug)
657     if format_ not in Book.formats:
658         raise Http404
659     if key != book.preview_key:
660         raise Http404
661     media_file = book.get_media(format_)
662     if not book.preview:
663         return HttpResponseRedirect(media_file.url)
664     return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
665
666
667 def download_zip(request, file_format=None, media_format=None, slug=None):
668     if file_format:
669         url = Book.zip_format(file_format)
670     elif media_format and slug is not None:
671         book = get_object_or_404(Book, slug=slug)
672         url = book.zip_audiobooks(media_format)
673     else:
674         raise Http404('No format specified for zip package')
675     return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
676
677
678 class CustomPDFFormView(AjaxableFormView):
679     form_class = forms.CustomPDFForm
680     title = gettext_lazy('Download custom PDF')
681     submit = gettext_lazy('Download')
682     template = 'catalogue/custom_pdf_form.html'
683     honeypot = True
684
685     def __call__(self, *args, **kwargs):
686         if settings.NO_CUSTOM_PDF:
687             raise Http404('Custom PDF is disabled')
688         return super(CustomPDFFormView, self).__call__(*args, **kwargs)
689
690     def form_args(self, request, obj):
691         """Override to parse view args and give additional args to the form."""
692         return (obj,), {}
693
694     def validate_object(self, obj, request):
695         book = obj
696         if not book.is_accessible_to(request.user):
697             return HttpResponseRedirect(book.get_absolute_url())
698         return super(CustomPDFFormView, self).validate_object(obj, request)
699
700     def get_object(self, request, slug, *args, **kwargs):
701         book = get_object_or_404(Book, slug=slug)
702         return book
703
704     def context_description(self, request, obj):
705         return obj.pretty_title()
706
707
708 def tag_catalogue(request, category):
709     if category == 'theme':
710         tags = Tag.objects.usage_for_model(
711             Fragment, counts=True).filter(category='theme')
712     else:
713         tags = list(get_top_level_related_tags((), categories=(category,)))
714
715     described_tags = [tag for tag in tags if tag.description]
716
717     if len(described_tags) > 4:
718         best = random.sample(described_tags, 4)
719     else:
720         best = described_tags
721
722     if request.EXPERIMENTS['layout'].value:
723         template_name = 'catalogue/2022/tag_catalogue.html'
724     else:
725         template_name = 'catalogue/tag_catalogue.html'
726
727     return render(request, template_name, {
728         'tags': tags,
729         'best': best,
730         'title': constants.CATEGORIES_NAME_PLURAL[category],
731         'whole_category': constants.WHOLE_CATEGORY[category],
732         'active_menu_item': 'theme' if category == 'theme' else None,
733     })
734
735
736 def collections(request):
737     objects = Collection.objects.filter(listed=True)
738
739     if len(objects) > 3:
740         best = random.sample(list(objects), 4)
741     else:
742         best = objects
743
744     if request.EXPERIMENTS['layout'].value:
745         template_name = 'catalogue/2022/collections.html'
746     else:
747         template_name = 'catalogue/collections.html'
748
749     return render(request, template_name, {
750         'objects': objects,
751         'best': best,
752         'active_menu_item': 'collections'
753     })
754
755
756 def ridero_cover(request, slug):
757     from librarian.cover import make_cover
758     wldoc = Book.objects.get(slug=slug).wldocument()
759     cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
760     response = HttpResponse(content_type="image/png")
761     cover.save(response)
762     return response
763
764
765 def get_isbn(request, book_format, slug):
766     book = Book.objects.get(slug=slug)
767     return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))