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 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
288 if self.ctx['work_tags']:
290 Q(book__in=qs) | Q(book__ancestor__in=qs)
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(
305 user=self.request.user
306 ).exclude(name='').exclude(pk__in=tag_ids)
309 fragments = Fragment.objects.filter(
310 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
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'))
322 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
323 list_type='books', extra=None):
326 tag_ids = [tag.pk for tag in tags]
328 related_tag_lists = []
330 related_tag_lists.append(related_tags)
332 related_tag_lists.append(
333 Tag.objects.usage_for_queryset(
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(
342 ).exclude(name='').exclude(pk__in=tag_ids)
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)
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')
359 categories = split_tags(*related_tag_lists)
361 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
362 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
364 objects = list(objects)
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))
371 best = random.sample(objects, 3)
376 'object_list': objects,
377 'categories': categories,
379 'list_type': list_type,
381 'main_tag': tags[0] if tags else None,
383 'formats_form': forms.DownloadFormatsForm(),
385 'active_menu_item': list_type,
390 if request.EXPERIMENTS['layout'].value:
391 has_theme = any(((theme := x).category == 'theme' for x in tags))
393 result['main_tag'] = theme
394 template = 'catalogue/2022/theme_detail.html'
396 template = 'catalogue/2022/author_detail.html'
398 template = 'catalogue/tagged_object_list.html'
401 request, template, result,
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([]))
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')
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(),
427 class ResponseInstead(Exception):
428 def __init__(self, response):
429 super(ResponseInstead, self).__init__()
430 self.response = response
433 def analyse_tags(request, tag_str):
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]))
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)])))
453 if len(tags) > settings.MAX_TAG_LIST:
455 except AttributeError:
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)
467 fragments = Fragment.tagged.with_all(fragment_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)
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))
480 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
481 'theme_is_set': True,
482 'active_menu_item': 'theme',
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)
491 tags = analyse_tags(request, tags)
492 except ResponseInstead as e:
495 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
498 if any(tag.category in ('theme', 'thing') for tag in tags):
499 return theme_list(request, tags, list_type=list_type)
501 if list_type == 'books':
502 books = Book.tagged.with_all(tags)
504 if any(tag.category == 'set' for tag in tags):
505 params = {'objects': books}
507 books = books.filter(findable=True)
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),
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()
518 'objects': Book.tagged.with_all(tags, audiobooks),
520 'daisy': Book.tagged.with_all(
521 tags, audiobooks.filter(media__type='daisy').distinct()
528 return object_list(request, tags=tags, list_type=list_type, **params)
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))
537 if request.EXPERIMENTS['layout'].value:
538 template_name = 'catalogue/2022/book_fragments.html'
540 template_name = 'catalogue/book_fragments.html'
548 'fragments': fragments,
549 'active_menu_item': 'books',
554 def book_detail(request, slug):
556 book = Book.objects.get(slug=slug)
557 except Book.DoesNotExist:
558 return pdcounter_views.book_stub_detail(request, slug)
560 new_layout = request.EXPERIMENTS['layout']
564 'catalogue/2022/book_detail.html' if new_layout.value else 'catalogue/book_detail.html',
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(),
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'):
582 audiobooks, projects, total_duration = book.get_audiobooks()
586 'catalogue/player.html',
590 'audiobooks': audiobooks,
591 'projects': projects,
595 def book_text(request, slug):
596 book = get_object_or_404(Book, slug=slug)
598 if not book.is_accessible_to(request.user):
599 return HttpResponseRedirect(book.get_absolute_url())
601 if not book.has_html_file():
603 with book.html_file.open('r') as f:
606 return render(request, 'catalogue/book_text.html', {
608 'book_text': book_text,
609 'inserts': DynamicTextInsert.get_all(request)
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():
623 book_import_form.save()
628 info = sys.exc_info()
629 exception = pprint.pformat(info[1])
630 tb = '\n'.join(traceback.format_tb(info[2]))
632 _("An error occurred: %(exception)s\n\n%(tb)s") % {
633 'exception': exception, 'tb': tb
635 content_type='text/plain'
637 return HttpResponse(_("Book imported successfully"))
638 return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
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})
650 def tag_info(request, tag_id):
651 tag = get_object_or_404(Tag, id=tag_id)
652 return HttpResponse(tag.description)
656 def embargo_link(request, key, format_, slug):
657 book = get_object_or_404(Book, slug=slug)
658 if format_ not in Book.formats:
660 if key != book.preview_key:
662 media_file = book.get_media(format_)
664 return HttpResponseRedirect(media_file.url)
665 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
668 def download_zip(request, file_format=None, media_format=None, slug=None):
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)
675 raise Http404('No format specified for zip package')
676 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
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'
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)
691 def form_args(self, request, obj):
692 """Override to parse view args and give additional args to the form."""
695 def validate_object(self, obj, request):
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)
701 def get_object(self, request, slug, *args, **kwargs):
702 book = get_object_or_404(Book, slug=slug)
705 def context_description(self, request, obj):
706 return obj.pretty_title()
709 def tag_catalogue(request, category):
710 if category == 'theme':
711 tags = Tag.objects.usage_for_model(
712 Fragment, counts=True).filter(category='theme')
714 tags = list(get_top_level_related_tags((), categories=(category,)))
716 described_tags = [tag for tag in tags if tag.description]
718 if len(described_tags) > 4:
719 best = random.sample(described_tags, 4)
721 best = described_tags
723 if request.EXPERIMENTS['layout'].value:
724 template_name = 'catalogue/2022/tag_catalogue.html'
726 template_name = 'catalogue/tag_catalogue.html'
728 return render(request, template_name, {
731 'title': constants.CATEGORIES_NAME_PLURAL[category],
732 'whole_category': constants.WHOLE_CATEGORY[category],
733 'active_menu_item': 'theme' if category == 'theme' else None,
737 def collections(request):
738 objects = Collection.objects.filter(listed=True)
741 best = random.sample(list(objects), 4)
745 if request.EXPERIMENTS['layout'].value:
746 template_name = 'catalogue/2022/collections.html'
748 template_name = 'catalogue/collections.html'
750 return render(request, template_name, {
753 'active_menu_item': 'collections'
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")
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))