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
118 def setup(self, request, **kwargs):
119 super().setup(request, **kwargs)
120 self.is_themed = False
124 def get_orderings(self):
125 order = self.get_order()
130 "active": k == order,
131 "default": v[0] is None,
133 for k, v in self.orderings.items()
137 order = self.request.GET.get('order')
138 if order not in self.orderings:
139 order = self.default_ordering
143 order_tag = self.get_order()
145 order = self.orderings[order_tag]
148 qs = qs.order_by(order_by)
151 def search(self, qs):
154 def get_template_names(self):
155 if is_ajax(self.request) or self.request.GET.get('dyn'):
157 return self.dynamic_themed_template_name
159 return self.dynamic_template_name
162 return self.themed_template_name
164 return self.template_name
166 def get_context_data(self, **kwargs):
167 ctx = super().get_context_data()
169 qs = self.get_queryset()
173 ctx['object_list'] = qs
174 ctx['suggested_tags'] = self.get_suggested_tags(qs)
175 ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
179 class BookList(ObjectListView):
180 title = gettext_lazy('Literature')
182 template_name = 'catalogue/2022/book_list.html'
183 dynamic_template_name = 'catalogue/2022/dynamic_book_list.html'
184 themed_template_name = 'catalogue/2022/themed_book_list.html'
185 dynamic_themed_template_name = 'catalogue/2022/dynamic_themed_book_list.html'
188 'pop': ('-popularity__count', 'najpopularniejsze'),
189 'alpha': (None, 'alfabetycznie'),
191 default_ordering = 'alpha'
193 def get_queryset(self):
194 return Book.objects.filter(parent=None, findable=True)
196 def search(self, qs):
197 term = self.request.GET.get('search')
199 meta_rels = TagRelation.objects.exclude(tag__category='set')
200 # TODO: search tags in currently displaying language
203 # meta=FilteredRelation('book__tag_relations', condition=Q(tag_relations__in=meta_rels))
206 Q(book__title__icontains=term) |
207 #Q(meta__tag_relations__tag__name_pl__icontains=term) |
208 Q(text__icontains=term)
212 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
214 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
218 class ArtList(ObjectListView):
219 template_name = 'catalogue/2022/book_list.html'
220 dynamic_template_name = 'catalogue/2022/dynamic_book_list.html'
221 title = gettext_lazy('Art')
222 list_type = 'gallery'
224 def get_queryset(self):
225 return Picture.objects.all()
227 def search(self, qs):
228 term = self.request.GET.get('search')
230 qs = qs.filter(Q(title__icontains=term) | Q(tag_relations__tag__name_pl__icontains=term)).distinct()
234 class LiteratureView(BookList):
235 def get_suggested_tags(self, queryset):
236 tags = list(get_top_level_related_tags([]))
237 tags.sort(key=lambda t: -t.count)
238 if self.request.user.is_authenticated:
239 tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
243 class AudiobooksView(LiteratureView):
244 title = gettext_lazy('Audiobooks')
246 def get_queryset(self):
247 return Book.objects.filter(findable=True, media__type='mp3').distinct()
250 class GalleryView(ArtList):
251 def get_suggested_tags(self, queryset):
252 return Tag.objects.usage_for_queryset(
255 ).exclude(pk__in=[t.id for t in self.ctx['tags']]).order_by('-count')
258 class TaggedObjectList(BookList):
259 def setup(self, request, tags, **kwargs):
260 super().setup(request, **kwargs)
261 self.ctx['tags'] = analyse_tags(request, tags)
262 self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
263 self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
264 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
265 self.ctx['main_tag'] = self.ctx['fragment_tags'][0] if self.is_themed else self.ctx['tags'][0]
266 self.ctx['filtering_tags'] = [
267 t for t in self.ctx['tags']
268 if t is not self.ctx['main_tag']
271 def get_queryset(self):
272 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
273 qs = qs.exclude(ancestor__in=qs)
275 qs = Fragment.tagged.with_all(self.ctx['fragment_tags']).filter(
276 Q(book__in=qs) | Q(book__ancestor__in=qs)
280 def get_suggested_tags(self, queryset):
281 tag_ids = [t.id for t in self.ctx['tags']]
282 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
283 if not self.is_themed:
284 if self.request.user.is_authenticated:
285 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
286 related_tags.extend(list(
287 Tag.objects.usage_for_queryset(
290 user=self.request.user
291 ).exclude(name='').exclude(pk__in=tag_ids)
294 fragments = Fragment.objects.filter(
295 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
298 Tag.objects.usage_for_queryset(
299 fragments, counts=True
300 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
301 .only('name', 'sort_key', 'category', 'slug'))
307 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
308 list_type='books', extra=None):
311 tag_ids = [tag.pk for tag in tags]
313 related_tag_lists = []
315 related_tag_lists.append(related_tags)
317 related_tag_lists.append(
318 Tag.objects.usage_for_queryset(
320 ).exclude(category='set').exclude(pk__in=tag_ids))
321 if request.user.is_authenticated:
322 related_tag_lists.append(
323 Tag.objects.usage_for_queryset(
327 ).exclude(name='').exclude(pk__in=tag_ids)
329 if not (extra and extra.get('theme_is_set')):
330 if fragments is None:
331 if list_type == 'gallery':
332 fragments = PictureArea.objects.filter(picture__in=objects)
334 fragments = Fragment.objects.filter(book__in=objects)
335 related_tag_lists.append(
336 Tag.objects.usage_for_queryset(
337 fragments, counts=True
338 ).filter(category='theme').exclude(pk__in=tag_ids)
339 .only('name', 'sort_key', 'category', 'slug'))
340 if isinstance(objects, QuerySet):
341 objects = prefetch_relations(objects, 'author')
344 categories = split_tags(*related_tag_lists)
346 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
347 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
349 objects = list(objects)
351 if not objects and len(tags) == 1 and list_type == 'books':
352 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
353 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
356 best = random.sample(objects, 3)
361 'object_list': objects,
362 'categories': categories,
364 'list_type': list_type,
366 'main_tag': tags[0] if tags else None,
368 'formats_form': forms.DownloadFormatsForm(),
370 'active_menu_item': list_type,
375 if request.EXPERIMENTS['layout'].value:
376 has_theme = any(((theme := x).category == 'theme' for x in tags))
378 result['main_tag'] = theme
379 template = 'catalogue/2022/theme_detail.html'
381 template = 'catalogue/2022/author_detail.html'
383 template = 'catalogue/tagged_object_list.html'
386 request, template, result,
390 def literature(request):
391 books = Book.objects.filter(parent=None, findable=True)
392 return object_list(request, books, related_tags=get_top_level_related_tags([]))
395 def gallery(request):
396 return object_list(request, Picture.objects.all(), list_type='gallery')
399 def audiobooks(request):
400 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
401 return object_list(request, audiobooks, list_type='audiobooks', extra={
402 'daisy': Book.objects.filter(findable=True, media__type='daisy').distinct(),
406 class ResponseInstead(Exception):
407 def __init__(self, response):
408 super(ResponseInstead, self).__init__()
409 self.response = response
412 def analyse_tags(request, tag_str):
414 tags = Tag.get_tag_list(tag_str)
415 except Tag.DoesNotExist:
416 # Perhaps the user is asking about an author in Public Domain
417 # counter (they are not represented in tags)
418 chunks = tag_str.split('/')
419 if len(chunks) == 2 and chunks[0] == 'autor':
420 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
422 except Tag.MultipleObjectsReturned as e:
423 # Ask the user to disambiguate
424 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
425 except Tag.UrlDeprecationWarning as e:
426 raise ResponseInstead(HttpResponsePermanentRedirect(
427 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
430 if len(tags) > settings.MAX_TAG_LIST:
432 except AttributeError:
438 def theme_list(request, tags, list_type):
439 shelf_tags = [tag for tag in tags if tag.category == 'set']
440 fragment_tags = [tag for tag in tags if tag.category != 'set']
441 if list_type == 'gallery':
442 fragments = PictureArea.tagged.with_all(fragment_tags)
444 fragments = Fragment.tagged.with_all(fragment_tags)
447 # TODO: Pictures on shelves not supported yet.
448 books = Book.tagged.with_all(shelf_tags).order_by()
449 fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
450 elif list_type == 'books':
451 fragments = fragments.filter(book__findable=True)
453 if not fragments and len(tags) == 1 and list_type == 'books':
454 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
455 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
457 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
458 'theme_is_set': True,
459 'active_menu_item': 'theme',
463 def tagged_object_list(request, tags, list_type):
465 tags = analyse_tags(request, tags)
466 except ResponseInstead as e:
469 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
472 if any(tag.category in ('theme', 'thing') for tag in tags):
473 return theme_list(request, tags, list_type=list_type)
475 if list_type == 'books':
476 books = Book.tagged.with_all(tags)
478 if any(tag.category == 'set' for tag in tags):
479 params = {'objects': books}
481 books = books.filter(findable=True)
483 'objects': Book.tagged_top_level(tags).filter(findable=True),
484 'fragments': Fragment.objects.filter(book__in=books),
485 'related_tags': get_top_level_related_tags(tags),
487 elif list_type == 'gallery':
488 params = {'objects': Picture.tagged.with_all(tags)}
489 elif list_type == 'audiobooks':
490 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
492 'objects': Book.tagged.with_all(tags, audiobooks),
494 'daisy': Book.tagged.with_all(
495 tags, audiobooks.filter(media__type='daisy').distinct()
502 return object_list(request, tags=tags, list_type=list_type, **params)
505 def book_fragments(request, slug, theme_slug):
506 book = get_object_or_404(Book, slug=slug)
507 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
508 fragments = Fragment.tagged.with_all([theme]).filter(
509 Q(book=book) | Q(book__ancestor=book))
511 if request.EXPERIMENTS['layout'].value:
512 template_name = 'catalogue/2022/book_fragments.html'
514 template_name = 'catalogue/book_fragments.html'
522 'fragments': fragments,
523 'active_menu_item': 'books',
528 def book_detail(request, slug):
530 book = Book.objects.get(slug=slug)
531 except Book.DoesNotExist:
532 return pdcounter_views.book_stub_detail(request, slug)
534 new_layout = request.EXPERIMENTS['layout']
538 'catalogue/2022/book_detail.html' if new_layout.value else 'catalogue/book_detail.html',
541 'accessible': book.is_accessible_to(request.user),
542 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
543 'active_menu_item': 'books',
544 'club_form': ScheduleForm() if book.preview else None,
545 'club': Club.objects.first() if book.preview else None,
546 'donation_form': DonationStep1Form(),
548 'EXPERIMENTS_SWITCHABLE_layout': True,
552 # używane w publicznym interfejsie
553 def player(request, slug):
554 book = get_object_or_404(Book, slug=slug)
555 if not book.has_media('mp3'):
558 audiobooks, projects, total_duration = book.get_audiobooks()
562 'catalogue/player.html',
566 'audiobooks': audiobooks,
567 'projects': projects,
571 def book_text(request, slug):
572 book = get_object_or_404(Book, slug=slug)
574 if not book.is_accessible_to(request.user):
575 return HttpResponseRedirect(book.get_absolute_url())
577 if not book.has_html_file():
579 with book.html_file.open('r') as f:
582 return render(request, 'catalogue/book_text.html', {
584 'book_text': book_text,
585 'inserts': DynamicTextInsert.get_all(request)
594 def import_book(request):
595 """docstring for import_book"""
596 book_import_form = forms.BookImportForm(request.POST, request.FILES)
597 if book_import_form.is_valid():
599 book_import_form.save()
604 info = sys.exc_info()
605 exception = pprint.pformat(info[1])
606 tb = '\n'.join(traceback.format_tb(info[2]))
608 _("An error occurred: %(exception)s\n\n%(tb)s") % {
609 'exception': exception, 'tb': tb
611 content_type='text/plain'
613 return HttpResponse(_("Book imported successfully"))
614 return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
619 def book_info(request, book_id, lang='pl'):
620 book = get_object_or_404(Book, id=book_id)
621 # set language by hand
622 translation.activate(lang)
623 return render(request, 'catalogue/book_info.html', {'book': book})
626 def tag_info(request, tag_id):
627 tag = get_object_or_404(Tag, id=tag_id)
628 return HttpResponse(tag.description)
632 def embargo_link(request, key, format_, slug):
633 book = get_object_or_404(Book, slug=slug)
634 if format_ not in Book.formats:
636 if key != book.preview_key:
638 media_file = book.get_media(format_)
640 return HttpResponseRedirect(media_file.url)
641 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
644 def download_zip(request, file_format=None, media_format=None, slug=None):
646 url = Book.zip_format(file_format)
647 elif media_format and slug is not None:
648 book = get_object_or_404(Book, slug=slug)
649 url = book.zip_audiobooks(media_format)
651 raise Http404('No format specified for zip package')
652 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
655 class CustomPDFFormView(AjaxableFormView):
656 form_class = forms.CustomPDFForm
657 title = gettext_lazy('Download custom PDF')
658 submit = gettext_lazy('Download')
659 template = 'catalogue/custom_pdf_form.html'
662 def __call__(self, *args, **kwargs):
663 if settings.NO_CUSTOM_PDF:
664 raise Http404('Custom PDF is disabled')
665 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
667 def form_args(self, request, obj):
668 """Override to parse view args and give additional args to the form."""
671 def validate_object(self, obj, request):
673 if not book.is_accessible_to(request.user):
674 return HttpResponseRedirect(book.get_absolute_url())
675 return super(CustomPDFFormView, self).validate_object(obj, request)
677 def get_object(self, request, slug, *args, **kwargs):
678 book = get_object_or_404(Book, slug=slug)
681 def context_description(self, request, obj):
682 return obj.pretty_title()
685 def tag_catalogue(request, category):
686 if category == 'theme':
687 tags = Tag.objects.usage_for_model(
688 Fragment, counts=True).filter(category='theme')
690 tags = list(get_top_level_related_tags((), categories=(category,)))
692 described_tags = [tag for tag in tags if tag.description]
694 if len(described_tags) > 4:
695 best = random.sample(described_tags, 4)
697 best = described_tags
699 if request.EXPERIMENTS['layout'].value:
700 template_name = 'catalogue/2022/tag_catalogue.html'
702 template_name = 'catalogue/tag_catalogue.html'
704 return render(request, template_name, {
707 'title': constants.CATEGORIES_NAME_PLURAL[category],
708 'whole_category': constants.WHOLE_CATEGORY[category],
709 'active_menu_item': 'theme' if category == 'theme' else None,
713 def collections(request):
714 objects = Collection.objects.filter(listed=True)
717 best = random.sample(list(objects), 4)
721 if request.EXPERIMENTS['layout'].value:
722 template_name = 'catalogue/2022/collections.html'
724 template_name = 'catalogue/collections.html'
726 return render(request, template_name, {
729 'active_menu_item': 'collections'
733 def ridero_cover(request, slug):
734 from librarian.cover import make_cover
735 wldoc = Book.objects.get(slug=slug).wldocument()
736 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
737 response = HttpResponse(content_type="image/png")
742 def get_isbn(request, book_format, slug):
743 book = Book.objects.get(slug=slug)
744 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))