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'])
272 self.ctx['main_tag'] = self.ctx['fragment_tags'][0] if self.is_themed else self.ctx['tags'][0]
273 self.ctx['filtering_tags'] = [
274 t for t in self.ctx['tags']
275 if t is not self.ctx['main_tag']
278 def get_queryset(self):
279 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
280 qs = qs.exclude(ancestor__in=qs)
282 qs = Fragment.tagged.with_all(self.ctx['fragment_tags']).filter(
283 Q(book__in=qs) | Q(book__ancestor__in=qs)
287 def get_suggested_tags(self, queryset):
288 tag_ids = [t.id for t in self.ctx['tags']]
289 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
290 if not self.is_themed:
291 if self.request.user.is_authenticated:
292 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
293 related_tags.extend(list(
294 Tag.objects.usage_for_queryset(
297 user=self.request.user
298 ).exclude(name='').exclude(pk__in=tag_ids)
301 fragments = Fragment.objects.filter(
302 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
305 Tag.objects.usage_for_queryset(
306 fragments, counts=True
307 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
308 .only('name', 'sort_key', 'category', 'slug'))
314 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
315 list_type='books', extra=None):
318 tag_ids = [tag.pk for tag in tags]
320 related_tag_lists = []
322 related_tag_lists.append(related_tags)
324 related_tag_lists.append(
325 Tag.objects.usage_for_queryset(
327 ).exclude(category='set').exclude(pk__in=tag_ids))
328 if request.user.is_authenticated:
329 related_tag_lists.append(
330 Tag.objects.usage_for_queryset(
334 ).exclude(name='').exclude(pk__in=tag_ids)
336 if not (extra and extra.get('theme_is_set')):
337 if fragments is None:
338 if list_type == 'gallery':
339 fragments = PictureArea.objects.filter(picture__in=objects)
341 fragments = Fragment.objects.filter(book__in=objects)
342 related_tag_lists.append(
343 Tag.objects.usage_for_queryset(
344 fragments, counts=True
345 ).filter(category='theme').exclude(pk__in=tag_ids)
346 .only('name', 'sort_key', 'category', 'slug'))
347 if isinstance(objects, QuerySet):
348 objects = prefetch_relations(objects, 'author')
351 categories = split_tags(*related_tag_lists)
353 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
354 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
356 objects = list(objects)
358 if not objects and len(tags) == 1 and list_type == 'books':
359 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
360 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
363 best = random.sample(objects, 3)
368 'object_list': objects,
369 'categories': categories,
371 'list_type': list_type,
373 'main_tag': tags[0] if tags else None,
375 'formats_form': forms.DownloadFormatsForm(),
377 'active_menu_item': list_type,
382 if request.EXPERIMENTS['layout'].value:
383 has_theme = any(((theme := x).category == 'theme' for x in tags))
385 result['main_tag'] = theme
386 template = 'catalogue/2022/theme_detail.html'
388 template = 'catalogue/2022/author_detail.html'
390 template = 'catalogue/tagged_object_list.html'
393 request, template, result,
397 def literature(request):
398 if request.EXPERIMENTS['layout'].value:
399 return LiteratureView.as_view()(request)
400 books = Book.objects.filter(parent=None, findable=True)
401 return object_list(request, books, related_tags=get_top_level_related_tags([]))
404 def gallery(request):
405 if request.EXPERIMENTS['layout'].value:
406 return GalleryView.as_view()(request)
407 return object_list(request, Picture.objects.all(), list_type='gallery')
410 def audiobooks(request):
411 if request.EXPERIMENTS['layout'].value:
412 return AudiobooksView.as_view()(request)
413 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
414 return object_list(request, audiobooks, list_type='audiobooks', extra={
415 'daisy': Book.objects.filter(findable=True, media__type='daisy').distinct(),
419 class ResponseInstead(Exception):
420 def __init__(self, response):
421 super(ResponseInstead, self).__init__()
422 self.response = response
425 def analyse_tags(request, tag_str):
427 tags = Tag.get_tag_list(tag_str)
428 except Tag.DoesNotExist:
429 # Perhaps the user is asking about an author in Public Domain
430 # counter (they are not represented in tags)
431 chunks = tag_str.split('/')
432 if len(chunks) == 2 and chunks[0] == 'autor':
433 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
435 except Tag.MultipleObjectsReturned as e:
436 # Ask the user to disambiguate
437 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
438 except Tag.UrlDeprecationWarning as e:
439 raise ResponseInstead(HttpResponsePermanentRedirect(
440 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
443 if len(tags) > settings.MAX_TAG_LIST:
445 except AttributeError:
451 def theme_list(request, tags, list_type):
452 shelf_tags = [tag for tag in tags if tag.category == 'set']
453 fragment_tags = [tag for tag in tags if tag.category != 'set']
454 if list_type == 'gallery':
455 fragments = PictureArea.tagged.with_all(fragment_tags)
457 fragments = Fragment.tagged.with_all(fragment_tags)
460 # TODO: Pictures on shelves not supported yet.
461 books = Book.tagged.with_all(shelf_tags).order_by()
462 fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
463 elif list_type == 'books':
464 fragments = fragments.filter(book__findable=True)
466 if not fragments and len(tags) == 1 and list_type == 'books':
467 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
468 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
470 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
471 'theme_is_set': True,
472 'active_menu_item': 'theme',
476 def tagged_object_list(request, tags, list_type):
477 if request.EXPERIMENTS['layout'].value and list_type in ('books', 'audiobooks'):
478 return TaggedObjectList.as_view()(request, tags=tags)
481 tags = analyse_tags(request, tags)
482 except ResponseInstead as e:
485 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
488 if any(tag.category in ('theme', 'thing') for tag in tags):
489 return theme_list(request, tags, list_type=list_type)
491 if list_type == 'books':
492 books = Book.tagged.with_all(tags)
494 if any(tag.category == 'set' for tag in tags):
495 params = {'objects': books}
497 books = books.filter(findable=True)
499 'objects': Book.tagged_top_level(tags).filter(findable=True),
500 'fragments': Fragment.objects.filter(book__in=books),
501 'related_tags': get_top_level_related_tags(tags),
503 elif list_type == 'gallery':
504 params = {'objects': Picture.tagged.with_all(tags)}
505 elif list_type == 'audiobooks':
506 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
508 'objects': Book.tagged.with_all(tags, audiobooks),
510 'daisy': Book.tagged.with_all(
511 tags, audiobooks.filter(media__type='daisy').distinct()
518 return object_list(request, tags=tags, list_type=list_type, **params)
521 def book_fragments(request, slug, theme_slug):
522 book = get_object_or_404(Book, slug=slug)
523 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
524 fragments = Fragment.tagged.with_all([theme]).filter(
525 Q(book=book) | Q(book__ancestor=book))
527 if request.EXPERIMENTS['layout'].value:
528 template_name = 'catalogue/2022/book_fragments.html'
530 template_name = 'catalogue/book_fragments.html'
538 'fragments': fragments,
539 'active_menu_item': 'books',
544 def book_detail(request, slug):
546 book = Book.objects.get(slug=slug)
547 except Book.DoesNotExist:
548 return pdcounter_views.book_stub_detail(request, slug)
550 new_layout = request.EXPERIMENTS['layout']
554 'catalogue/2022/book_detail.html' if new_layout.value else 'catalogue/book_detail.html',
557 'accessible': book.is_accessible_to(request.user),
558 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
559 'active_menu_item': 'books',
560 'club_form': ScheduleForm() if book.preview else None,
561 'club': Club.objects.first() if book.preview else None,
562 'donation_form': DonationStep1Form(),
564 'EXPERIMENTS_SWITCHABLE_layout': True,
568 # używane w publicznym interfejsie
569 def player(request, slug):
570 book = get_object_or_404(Book, slug=slug)
571 if not book.has_media('mp3'):
574 audiobooks, projects, total_duration = book.get_audiobooks()
578 'catalogue/player.html',
582 'audiobooks': audiobooks,
583 'projects': projects,
587 def book_text(request, slug):
588 book = get_object_or_404(Book, slug=slug)
590 if not book.is_accessible_to(request.user):
591 return HttpResponseRedirect(book.get_absolute_url())
593 if not book.has_html_file():
595 with book.html_file.open('r') as f:
598 return render(request, 'catalogue/book_text.html', {
600 'book_text': book_text,
601 'inserts': DynamicTextInsert.get_all(request)
610 def import_book(request):
611 """docstring for import_book"""
612 book_import_form = forms.BookImportForm(request.POST, request.FILES)
613 if book_import_form.is_valid():
615 book_import_form.save()
620 info = sys.exc_info()
621 exception = pprint.pformat(info[1])
622 tb = '\n'.join(traceback.format_tb(info[2]))
624 _("An error occurred: %(exception)s\n\n%(tb)s") % {
625 'exception': exception, 'tb': tb
627 content_type='text/plain'
629 return HttpResponse(_("Book imported successfully"))
630 return HttpResponse(_("Error importing file: %r") % book_import_form.errors)
635 def book_info(request, book_id, lang='pl'):
636 book = get_object_or_404(Book, id=book_id)
637 # set language by hand
638 translation.activate(lang)
639 return render(request, 'catalogue/book_info.html', {'book': book})
642 def tag_info(request, tag_id):
643 tag = get_object_or_404(Tag, id=tag_id)
644 return HttpResponse(tag.description)
648 def embargo_link(request, key, format_, slug):
649 book = get_object_or_404(Book, slug=slug)
650 if format_ not in Book.formats:
652 if key != book.preview_key:
654 media_file = book.get_media(format_)
656 return HttpResponseRedirect(media_file.url)
657 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
660 def download_zip(request, file_format=None, media_format=None, slug=None):
662 url = Book.zip_format(file_format)
663 elif media_format and slug is not None:
664 book = get_object_or_404(Book, slug=slug)
665 url = book.zip_audiobooks(media_format)
667 raise Http404('No format specified for zip package')
668 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
671 class CustomPDFFormView(AjaxableFormView):
672 form_class = forms.CustomPDFForm
673 title = gettext_lazy('Download custom PDF')
674 submit = gettext_lazy('Download')
675 template = 'catalogue/custom_pdf_form.html'
678 def __call__(self, *args, **kwargs):
679 if settings.NO_CUSTOM_PDF:
680 raise Http404('Custom PDF is disabled')
681 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
683 def form_args(self, request, obj):
684 """Override to parse view args and give additional args to the form."""
687 def validate_object(self, obj, request):
689 if not book.is_accessible_to(request.user):
690 return HttpResponseRedirect(book.get_absolute_url())
691 return super(CustomPDFFormView, self).validate_object(obj, request)
693 def get_object(self, request, slug, *args, **kwargs):
694 book = get_object_or_404(Book, slug=slug)
697 def context_description(self, request, obj):
698 return obj.pretty_title()
701 def tag_catalogue(request, category):
702 if category == 'theme':
703 tags = Tag.objects.usage_for_model(
704 Fragment, counts=True).filter(category='theme')
706 tags = list(get_top_level_related_tags((), categories=(category,)))
708 described_tags = [tag for tag in tags if tag.description]
710 if len(described_tags) > 4:
711 best = random.sample(described_tags, 4)
713 best = described_tags
715 if request.EXPERIMENTS['layout'].value:
716 template_name = 'catalogue/2022/tag_catalogue.html'
718 template_name = 'catalogue/tag_catalogue.html'
720 return render(request, template_name, {
723 'title': constants.CATEGORIES_NAME_PLURAL[category],
724 'whole_category': constants.WHOLE_CATEGORY[category],
725 'active_menu_item': 'theme' if category == 'theme' else None,
729 def collections(request):
730 objects = Collection.objects.filter(listed=True)
733 best = random.sample(list(objects), 4)
737 if request.EXPERIMENTS['layout'].value:
738 template_name = 'catalogue/2022/collections.html'
740 template_name = 'catalogue/collections.html'
742 return render(request, template_name, {
745 'active_menu_item': 'collections'
749 def ridero_cover(request, slug):
750 from librarian.cover import make_cover
751 wldoc = Book.objects.get(slug=slug).wldocument()
752 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
753 response = HttpResponse(content_type="image/png")
758 def get_isbn(request, book_format, slug):
759 book = Book.objects.get(slug=slug)
760 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))