1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 from collections import OrderedDict
7 from urllib.parse import quote_plus
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
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
36 staff_required = user_passes_test(lambda user: user.is_staff)
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',
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,
62 'books_by_parent': books_by_parent,
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')
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'
78 template_name = 'catalogue/collection.html'
79 return render(request, template_name, {
81 'active_menu_item': 'collections',
85 def differentiate_tags(request, tags, ambiguous_slugs):
86 beginning = '/'.join(tag.url_chunk for tag in tags)
87 unparsed = '/'.join(ambiguous_slugs[1:])
89 for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
91 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
96 'catalogue/differentiate_tags.html',
97 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
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
108 vary_on_headers('X-Requested-With'),
109 cache_control(max_age=1),
111 class ObjectListView(TemplateView):
113 themed_page_size = 10
114 item_template_name = ''
116 default_ordering = None
119 self.is_themed = False
123 def dispatch(self, *args, **kwargs):
126 except ResponseInstead as e:
128 return super().dispatch(*args, **kwargs)
130 def get_orderings(self):
131 order = self.get_order()
136 "active": k == order,
137 "default": v[0] is None,
139 for k, v in self.orderings.items()
143 order = self.request.GET.get('order')
144 if order not in self.orderings:
145 order = self.default_ordering
149 order_tag = self.get_order()
151 order = self.orderings[order_tag]
154 qs = qs.order_by(order_by)
157 def search(self, qs):
160 def get_template_names(self):
161 if is_ajax(self.request) or self.request.GET.get('dyn'):
163 return self.dynamic_themed_template_name
165 return self.dynamic_template_name
168 return self.themed_template_name
170 return self.template_name
172 def get_context_data(self, **kwargs):
173 ctx = super().get_context_data()
175 qs = self.get_queryset()
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'])
185 class BookList(ObjectListView):
186 title = gettext_lazy('Literature')
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'
194 'pop': ('-popularity__count', 'najpopularniejsze'),
195 'alpha': (None, 'alfabetycznie'),
197 default_ordering = 'alpha'
199 def get_queryset(self):
200 return Book.objects.filter(parent=None, findable=True)
202 def search(self, qs):
203 term = self.request.GET.get('search')
205 meta_rels = TagRelation.objects.exclude(tag__category='set')
206 # TODO: search tags in currently displaying language
209 # meta=FilteredRelation('book__tag_relations', condition=Q(tag_relations__in=meta_rels))
212 Q(book__title__icontains=term) |
213 #Q(meta__tag_relations__tag__name_pl__icontains=term) |
214 Q(text__icontains=term)
218 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
220 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
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'
230 def get_queryset(self):
231 return Picture.objects.all()
233 def search(self, qs):
234 term = self.request.GET.get('search')
236 qs = qs.filter(Q(title__icontains=term) | Q(tag_relations__tag__name_pl__icontains=term)).distinct()
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='')))
249 class AudiobooksView(LiteratureView):
250 title = gettext_lazy('Audiobooks')
251 list_type = 'audiobooks'
253 def get_queryset(self):
254 return Book.objects.filter(findable=True, media__type='mp3').distinct()
257 class GalleryView(ArtList):
258 def get_suggested_tags(self, queryset):
259 return Tag.objects.usage_for_queryset(
262 ).exclude(pk__in=[t.id for t in self.ctx['tags']]).order_by('-count')
265 class TaggedObjectList(BookList):
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'])
273 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
274 elif self.ctx['tags']:
275 self.ctx['main_tag'] = self.ctx['tags'][0]
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']
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)
287 qs = Fragment.tagged.with_all(self.ctx['fragment_tags']).filter(
288 Q(book__in=qs) | Q(book__ancestor__in=qs)
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(
302 user=self.request.user
303 ).exclude(name='').exclude(pk__in=tag_ids)
306 fragments = Fragment.objects.filter(
307 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
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'))
319 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
320 list_type='books', extra=None):
323 tag_ids = [tag.pk for tag in tags]
325 related_tag_lists = []
327 related_tag_lists.append(related_tags)
329 related_tag_lists.append(
330 Tag.objects.usage_for_queryset(
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(
339 ).exclude(name='').exclude(pk__in=tag_ids)
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)
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')
356 categories = split_tags(*related_tag_lists)
358 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
359 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
361 objects = list(objects)
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))
368 best = random.sample(objects, 3)
373 'object_list': objects,
374 'categories': categories,
376 'list_type': list_type,
378 'main_tag': tags[0] if tags else None,
380 'formats_form': forms.DownloadFormatsForm(),
382 'active_menu_item': list_type,
387 if request.EXPERIMENTS['layout'].value:
388 has_theme = any(((theme := x).category == 'theme' for x in tags))
390 result['main_tag'] = theme
391 template = 'catalogue/2022/theme_detail.html'
393 template = 'catalogue/2022/author_detail.html'
395 template = 'catalogue/tagged_object_list.html'
398 request, template, result,
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([]))
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')
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(),
424 class ResponseInstead(Exception):
425 def __init__(self, response):
426 super(ResponseInstead, self).__init__()
427 self.response = response
430 def analyse_tags(request, tag_str):
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]))
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)])))
450 if len(tags) > settings.MAX_TAG_LIST:
452 except AttributeError:
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)
464 fragments = Fragment.tagged.with_all(fragment_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)
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))
477 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
478 'theme_is_set': True,
479 'active_menu_item': 'theme',
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)
488 tags = analyse_tags(request, tags)
489 except ResponseInstead as e:
492 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
495 if any(tag.category in ('theme', 'thing') for tag in tags):
496 return theme_list(request, tags, list_type=list_type)
498 if list_type == 'books':
499 books = Book.tagged.with_all(tags)
501 if any(tag.category == 'set' for tag in tags):
502 params = {'objects': books}
504 books = books.filter(findable=True)
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),
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()
515 'objects': Book.tagged.with_all(tags, audiobooks),
517 'daisy': Book.tagged.with_all(
518 tags, audiobooks.filter(media__type='daisy').distinct()
525 return object_list(request, tags=tags, list_type=list_type, **params)
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))
534 if request.EXPERIMENTS['layout'].value:
535 template_name = 'catalogue/2022/book_fragments.html'
537 template_name = 'catalogue/book_fragments.html'
545 'fragments': fragments,
546 'active_menu_item': 'books',
551 def book_detail(request, slug):
553 book = Book.objects.get(slug=slug)
554 except Book.DoesNotExist:
555 return pdcounter_views.book_stub_detail(request, slug)
557 new_layout = request.EXPERIMENTS['layout']
561 'catalogue/2022/book_detail.html' if new_layout.value else 'catalogue/book_detail.html',
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(),
573 # używane w publicznym interfejsie
574 def player(request, slug):
575 book = get_object_or_404(Book, slug=slug)
576 if not book.has_media('mp3'):
579 audiobooks, projects, total_duration = book.get_audiobooks()
583 'catalogue/player.html',
587 'audiobooks': audiobooks,
588 'projects': projects,
592 def book_text(request, slug):
593 book = get_object_or_404(Book, slug=slug)
595 if not book.is_accessible_to(request.user):
596 return HttpResponseRedirect(book.get_absolute_url())
598 if not book.has_html_file():
600 with book.html_file.open('r') as f:
603 return render(request, 'catalogue/book_text.html', {
605 'book_text': book_text,
606 'inserts': DynamicTextInsert.get_all(request)
615 def import_book(request):
616 """docstring for import_book"""
617 book_import_form = forms.BookImportForm(request.POST, request.FILES)
618 if book_import_form.is_valid():
620 book_import_form.save()
625 info = sys.exc_info()
626 exception = pprint.pformat(info[1])
627 tb = '\n'.join(traceback.format_tb(info[2]))
629 _("An error occurred: %(exception)s\n\n%(tb)s") % {
630 'exception': exception, 'tb': tb
632 content_type='text/plain'
634 return HttpResponse(_("Book imported successfully"))
635 return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
640 def book_info(request, book_id, lang='pl'):
641 book = get_object_or_404(Book, id=book_id)
642 # set language by hand
643 translation.activate(lang)
644 return render(request, 'catalogue/book_info.html', {'book': book})
647 def tag_info(request, tag_id):
648 tag = get_object_or_404(Tag, id=tag_id)
649 return HttpResponse(tag.description)
653 def embargo_link(request, key, format_, slug):
654 book = get_object_or_404(Book, slug=slug)
655 if format_ not in Book.formats:
657 if key != book.preview_key:
659 media_file = book.get_media(format_)
661 return HttpResponseRedirect(media_file.url)
662 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
665 def download_zip(request, file_format=None, media_format=None, slug=None):
667 url = Book.zip_format(file_format)
668 elif media_format and slug is not None:
669 book = get_object_or_404(Book, slug=slug)
670 url = book.zip_audiobooks(media_format)
672 raise Http404('No format specified for zip package')
673 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
676 class CustomPDFFormView(AjaxableFormView):
677 form_class = forms.CustomPDFForm
678 title = gettext_lazy('Download custom PDF')
679 submit = gettext_lazy('Download')
680 template = 'catalogue/custom_pdf_form.html'
683 def __call__(self, *args, **kwargs):
684 if settings.NO_CUSTOM_PDF:
685 raise Http404('Custom PDF is disabled')
686 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
688 def form_args(self, request, obj):
689 """Override to parse view args and give additional args to the form."""
692 def validate_object(self, obj, request):
694 if not book.is_accessible_to(request.user):
695 return HttpResponseRedirect(book.get_absolute_url())
696 return super(CustomPDFFormView, self).validate_object(obj, request)
698 def get_object(self, request, slug, *args, **kwargs):
699 book = get_object_or_404(Book, slug=slug)
702 def context_description(self, request, obj):
703 return obj.pretty_title()
706 def tag_catalogue(request, category):
707 if category == 'theme':
708 tags = Tag.objects.usage_for_model(
709 Fragment, counts=True).filter(category='theme')
711 tags = list(get_top_level_related_tags((), categories=(category,)))
713 described_tags = [tag for tag in tags if tag.description]
715 if len(described_tags) > 4:
716 best = random.sample(described_tags, 4)
718 best = described_tags
720 if request.EXPERIMENTS['layout'].value:
721 template_name = 'catalogue/2022/tag_catalogue.html'
723 template_name = 'catalogue/tag_catalogue.html'
725 return render(request, template_name, {
728 'title': constants.CATEGORIES_NAME_PLURAL[category],
729 'whole_category': constants.WHOLE_CATEGORY[category],
730 'active_menu_item': 'theme' if category == 'theme' else None,
734 def collections(request):
735 objects = Collection.objects.filter(listed=True)
738 best = random.sample(list(objects), 4)
742 if request.EXPERIMENTS['layout'].value:
743 template_name = 'catalogue/2022/collections.html'
745 template_name = 'catalogue/collections.html'
747 return render(request, template_name, {
750 'active_menu_item': 'collections'
754 def ridero_cover(request, slug):
755 from librarian.cover import make_cover
756 wldoc = Book.objects.get(slug=slug).wldocument()
757 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
758 response = HttpResponse(content_type="image/png")
763 def get_isbn(request, book_format, slug):
764 book = Book.objects.get(slug=slug)
765 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))