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(),
571 'EXPERIMENTS_SWITCHABLE_layout': True,
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'):
581 audiobooks, projects, total_duration = book.get_audiobooks()
585 'catalogue/player.html',
589 'audiobooks': audiobooks,
590 'projects': projects,
594 def book_text(request, slug):
595 book = get_object_or_404(Book, slug=slug)
597 if not book.is_accessible_to(request.user):
598 return HttpResponseRedirect(book.get_absolute_url())
600 if not book.has_html_file():
602 with book.html_file.open('r') as f:
605 return render(request, 'catalogue/book_text.html', {
607 'book_text': book_text,
608 'inserts': DynamicTextInsert.get_all(request)
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():
622 book_import_form.save()
627 info = sys.exc_info()
628 exception = pprint.pformat(info[1])
629 tb = '\n'.join(traceback.format_tb(info[2]))
631 _("An error occurred: %(exception)s\n\n%(tb)s") % {
632 'exception': exception, 'tb': tb
634 content_type='text/plain'
636 return HttpResponse(_("Book imported successfully"))
637 return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
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})
649 def tag_info(request, tag_id):
650 tag = get_object_or_404(Tag, id=tag_id)
651 return HttpResponse(tag.description)
655 def embargo_link(request, key, format_, slug):
656 book = get_object_or_404(Book, slug=slug)
657 if format_ not in Book.formats:
659 if key != book.preview_key:
661 media_file = book.get_media(format_)
663 return HttpResponseRedirect(media_file.url)
664 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
667 def download_zip(request, file_format=None, media_format=None, slug=None):
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)
674 raise Http404('No format specified for zip package')
675 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
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'
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)
690 def form_args(self, request, obj):
691 """Override to parse view args and give additional args to the form."""
694 def validate_object(self, obj, request):
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)
700 def get_object(self, request, slug, *args, **kwargs):
701 book = get_object_or_404(Book, slug=slug)
704 def context_description(self, request, obj):
705 return obj.pretty_title()
708 def tag_catalogue(request, category):
709 if category == 'theme':
710 tags = Tag.objects.usage_for_model(
711 Fragment, counts=True).filter(category='theme')
713 tags = list(get_top_level_related_tags((), categories=(category,)))
715 described_tags = [tag for tag in tags if tag.description]
717 if len(described_tags) > 4:
718 best = random.sample(described_tags, 4)
720 best = described_tags
722 if request.EXPERIMENTS['layout'].value:
723 template_name = 'catalogue/2022/tag_catalogue.html'
725 template_name = 'catalogue/tag_catalogue.html'
727 return render(request, template_name, {
730 'title': constants.CATEGORIES_NAME_PLURAL[category],
731 'whole_category': constants.WHOLE_CATEGORY[category],
732 'active_menu_item': 'theme' if category == 'theme' else None,
736 def collections(request):
737 objects = Collection.objects.filter(listed=True)
740 best = random.sample(list(objects), 4)
744 if request.EXPERIMENTS['layout'].value:
745 template_name = 'catalogue/2022/collections.html'
747 template_name = 'catalogue/collections.html'
749 return render(request, template_name, {
752 'active_menu_item': 'collections'
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")
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))