1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
4 from collections import OrderedDict
7 from urllib.parse import quote_plus
8 from slugify import slugify
9 from zipstream import ZipStream
11 from django.conf import settings
12 from django.template.loader import render_to_string
13 from django.shortcuts import get_object_or_404, render, redirect
14 from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect, StreamingHttpResponse
15 from django.urls import reverse
16 from django.db.models import Q, QuerySet
17 from django.contrib.auth.decorators import login_required, user_passes_test
18 from django.utils import translation
19 from django.utils.translation import gettext_lazy
20 from django.views.decorators.cache import never_cache
21 from django.views.generic import TemplateView
23 from ajaxable.utils import AjaxableFormView
24 from club.forms import DonationStep1Form
25 from club.models import Club
26 from annoy.models import DynamicTextInsert
27 from pdcounter import views as pdcounter_views
28 from social.models import UserList
29 from wolnelektury.utils import is_ajax
30 from catalogue import constants
31 from catalogue import forms
32 from catalogue.helpers import get_top_level_related_tags
33 from catalogue.models import Book, Collection, Tag, Fragment
34 from catalogue.models.tag import TagRelation
35 from catalogue.utils import split_tags
36 from catalogue.models.tag import prefetch_relations
38 staff_required = user_passes_test(lambda user: user.is_staff)
41 def catalogue(request):
42 return render(request, 'catalogue/catalogue.html', {
43 'books': Book.objects.filter(findable=True, parent=None),
44 'collections': Collection.objects.filter(listed=True),
48 def daisy_list(request):
49 return object_list(request, Book.objects.filter(media__type='daisy'))
52 def collection(request, slug):
53 coll = get_object_or_404(Collection, slug=slug)
54 template_name = 'catalogue/collection.html'
55 return render(request, template_name, {
60 def differentiate_tags(request, tags, ambiguous_slugs):
61 beginning = '/'.join(tag.url_chunk for tag in tags)
62 unparsed = '/'.join(ambiguous_slugs[1:])
64 for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
66 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
71 'catalogue/differentiate_tags.html',
72 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
76 from django.db.models import FilteredRelation, Q
77 from django.views.decorators.cache import cache_control
78 from django.views.decorators.vary import vary_on_headers
79 from django.utils.decorators import method_decorator
83 vary_on_headers('X-Requested-With'),
84 cache_control(max_age=1),
86 class ObjectListView(TemplateView):
89 item_template_name = ''
91 default_ordering = None
94 self.is_themed = False
98 def dispatch(self, *args, **kwargs):
101 except ResponseInstead as e:
103 return super().dispatch(*args, **kwargs)
105 def get_orderings(self):
106 order = self.get_order()
111 "active": k == order,
112 "default": v[0] is None,
114 for k, v in self.orderings.items()
118 order = self.request.GET.get('order')
119 if order not in self.orderings:
120 order = self.default_ordering
124 order_tag = self.get_order()
126 order = self.orderings[order_tag]
129 qs = qs.order_by(order_by)
132 def search(self, qs):
135 def get_template_names(self):
136 if is_ajax(self.request) or self.request.GET.get('dyn'):
138 return self.dynamic_themed_template_name
140 return self.dynamic_template_name
143 return self.themed_template_name
145 return self.template_name
147 def get_context_data(self, **kwargs):
148 ctx = super().get_context_data()
150 qs = self.get_queryset()
154 ctx['object_list'] = qs
155 ctx['suggested_tags'] = self.get_suggested_tags(qs)
156 ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
160 class BookList(ObjectListView):
161 title = gettext_lazy('Literatura')
163 template_name = 'catalogue/book_list.html'
164 dynamic_template_name = 'catalogue/dynamic_book_list.html'
165 themed_template_name = 'catalogue/themed_book_list.html'
166 dynamic_themed_template_name = 'catalogue/dynamic_themed_book_list.html'
169 'pop': ('-popularity__count', gettext_lazy('najpopularniejsze')),
170 'alpha': (None, gettext_lazy('alfabetycznie')),
172 default_ordering = 'alpha'
174 def get_queryset(self):
175 return Book.objects.filter(parent=None, findable=True)
177 def search(self, qs):
178 term = self.request.GET.get('search')
180 meta_rels = TagRelation.objects.filter(tag__category='author')
181 # TODO: search tags in currently displaying language
183 rels = meta_rels.filter(tag__name_pl__icontains=term)
185 Q(book__title__icontains=term) |
186 Q(tag_relations__in=rels) |
187 Q(text__icontains=term)
191 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
193 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
197 class LiteratureView(BookList):
198 def get_suggested_tags(self, queryset):
199 tags = list(get_top_level_related_tags([]))
200 tags.sort(key=lambda t: -t.count)
201 if self.request.user.is_authenticated:
202 tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
206 class AudiobooksView(LiteratureView):
207 title = gettext_lazy('Audiobooki')
208 list_type = 'audiobooks'
210 def get_queryset(self):
211 return Book.objects.filter(findable=True, media__type='mp3').distinct()
214 class TaggedObjectList(BookList):
218 self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
219 if len(self.ctx['tags']) > 4:
226 for tag in self.ctx['tags']:
227 if isinstance(tag, UserList):
228 self.ctx['user_lists'].append(tag)
229 elif tag.category == 'theme':
230 self.ctx['fragment_tags'].append(tag)
232 self.ctx['work_tags'].append(tag)
234 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
236 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
237 elif self.ctx['work_tags']:
238 self.ctx['main_tag'] = self.ctx['work_tags'][0]
240 self.ctx['main_tag'] = None
241 self.ctx['filtering_tags'] = [
242 t for t in self.ctx['tags']
243 if t is not self.ctx['main_tag']
245 if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None and self.ctx['main_tag'].category == 'author':
246 self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
247 self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
249 def get_queryset(self):
250 if self.ctx['work_tags']:
251 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
253 qs = Book.objects.filter(findable=True)
254 for ul in self.ctx['user_lists']:
255 qs = qs.filter(id__in=[i.id for i in ul.get_books()])
256 qs = qs.exclude(ancestor__in=qs)
258 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
259 if self.ctx['work_tags']:
261 Q(book__in=qs) | Q(book__ancestor__in=qs)
266 def get_suggested_tags(self, queryset):
267 if self.ctx['user_lists']:
270 tag_ids = [t.id for t in self.ctx['tags']]
273 current_books = self.get_queryset().values_list('book', flat=True).distinct()
274 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
276 related_tags.extend(list(
277 Tag.objects.usage_for_queryset(
279 ).exclude(category='set').exclude(pk__in=tag_ids)
281 ### FIXME: These won't be tags
282 if self.request.user.is_authenticated:
283 related_tags.extend(list(
284 Tag.objects.usage_for_queryset(
287 user=self.request.user
288 ).exclude(name='').exclude(pk__in=tag_ids)
291 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
292 if self.request.user.is_authenticated:
293 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
294 related_tags.extend(list(
295 Tag.objects.usage_for_queryset(
298 user=self.request.user
299 ).exclude(name='').exclude(pk__in=tag_ids)
302 fragments = Fragment.objects.filter(
303 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
306 Tag.objects.usage_for_queryset(
307 fragments, counts=True
308 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
309 .only('name', 'sort_key', 'category', 'slug'))
315 def object_list(request, objects, list_type='books'):
316 related_tag_lists = []
318 related_tag_lists.append(
319 Tag.objects.usage_for_queryset(
321 ).exclude(category='set'))
322 ### FIXME: these won't be tags
323 if request.user.is_authenticated:
324 related_tag_lists.append(
325 Tag.objects.usage_for_queryset(
332 fragments = Fragment.objects.filter(book__in=objects)
333 related_tag_lists.append(
334 Tag.objects.usage_for_queryset(
335 fragments, counts=True
336 ).filter(category='theme')
337 .only('name', 'sort_key', 'category', 'slug'))
338 if isinstance(objects, QuerySet):
339 objects = prefetch_relations(objects, 'author')
341 categories = split_tags(*related_tag_lists)
343 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
344 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
346 objects = list(objects)
349 'object_list': objects,
351 'list_type': list_type,
354 template = 'catalogue/author_detail.html'
357 request, template, result,
361 class ResponseInstead(Exception):
362 def __init__(self, response):
363 super(ResponseInstead, self).__init__()
364 self.response = response
367 def analyse_tags(request, tag_str):
369 tags = Tag.get_tag_list(tag_str)
370 except Tag.DoesNotExist:
371 # Perhaps the user is asking about an author in Public Domain
372 # counter (they are not represented in tags)
373 chunks = tag_str.split('/')
374 if len(chunks) == 2 and chunks[0] == 'autor':
375 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
377 except Tag.MultipleObjectsReturned as e:
378 # Ask the user to disambiguate
379 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
380 except Tag.UrlDeprecationWarning as e:
381 raise ResponseInstead(HttpResponsePermanentRedirect(
382 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
387 if len(tags) > settings.MAX_TAG_LIST:
389 except AttributeError:
395 def tagged_object_list(request, tags, list_type):
396 return TaggedObjectList.as_view()(request, tags=tags)
399 def book_fragments(request, slug, theme_slug):
400 book = get_object_or_404(Book, slug=slug)
401 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
402 fragments = Fragment.tagged.with_all([theme]).filter(
403 Q(book=book) | Q(book__ancestor=book))
405 template_name = 'catalogue/book_fragments.html'
412 'fragments': fragments,
417 def book_detail(request, slug):
419 book = Book.objects.get(slug=slug)
420 except Book.DoesNotExist:
421 return pdcounter_views.book_stub_detail(request, slug)
425 'catalogue/book_detail.html',
428 'accessible': book.is_accessible_to(request.user),
429 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
430 'club': Club.objects.first() if book.preview else None,
431 'donation_form': DonationStep1Form(),
435 def book_text(request, slug):
436 book = get_object_or_404(Book, slug=slug)
438 if not book.is_accessible_to(request.user):
439 return HttpResponseRedirect(book.get_absolute_url())
441 if not book.has_html_file():
443 with book.html_file.open('r') as f:
446 return render(request, 'catalogue/book_text.html', {
448 'extra_info': book.get_extra_info_json(),
449 'book_text': book_text,
450 'inserts': DynamicTextInsert.get_all(request),
452 'club': Club.objects.first(),
453 'donation_form': DonationStep1Form(),
462 def import_book(request):
463 """docstring for import_book"""
464 book_import_form = forms.BookImportForm(request.POST, request.FILES)
465 if book_import_form.is_valid():
467 book_import_form.save()
472 info = sys.exc_info()
473 exception = pprint.pformat(info[1])
474 tb = '\n'.join(traceback.format_tb(info[2]))
476 "Błąd: %(exception)s\n\n%(tb)s" % {
477 'exception': exception, 'tb': tb
479 content_type='text/plain'
481 return HttpResponse("Książka zaimportowana")
482 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
487 def book_info(request, book_id, lang='pl'):
488 book = get_object_or_404(Book, id=book_id)
489 # set language by hand
490 translation.activate(lang)
491 return render(request, 'catalogue/book_info.html', {'book': book})
494 def tag_info(request, tag_id):
495 tag = get_object_or_404(Tag, id=tag_id)
496 return HttpResponse(tag.description)
500 def embargo_link(request, key, format_, slug):
501 book = get_object_or_404(Book, slug=slug)
502 if format_ not in Book.formats:
504 if key != book.preview_key:
506 media_file = book.get_media(format_)
508 return HttpResponseRedirect(media_file.url)
509 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
512 def download_zip(request, file_format=None, media_format=None, slug=None):
514 url = Book.zip_format(file_format)
515 elif media_format and slug is not None:
516 book = get_object_or_404(Book, slug=slug)
517 url = book.zip_audiobooks(media_format)
519 raise Http404('No format specified for zip package')
520 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
523 def stream_zip(request, media_format=None, slug=None):
524 book = get_object_or_404(Book, slug=slug)
525 def iterate_audiobooks(book, names):
526 for bm in book.media.filter(type=media_format).order_by('index'):
529 bm.get_extra_info_json().get('license'),
530 names + (slugify(bm.part_name),) if bm.part_name else names
532 for child in book.get_children():
533 yield from iterate_audiobooks(child, names + (slugify(child.title),))
538 for i, (file_path, lic, names) in enumerate(iterate_audiobooks(book, ())):
540 part_name = '_'.join(names)
541 ext = file_path.rsplit('.', 1)[-1]
542 zip_name = f'{book.slug}_{index:03d}_{part_name}'[:240] + '.' + ext
543 zs.add_path(file_path, zip_name)
545 lic_name = constants.LICENSES.get(lic, {}).get('locative')
547 licenses.add(lic_name)
549 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
550 'licenses': licenses,
552 zs.add(readme, 'informacje.txt')
554 response = StreamingHttpResponse(zs, content_type='application/zip')
555 response['Content-Disposition'] = f'attachment; filename={slug}_{media_format}.zip'
559 class CustomPDFFormView(AjaxableFormView):
560 form_class = forms.CustomPDFForm
561 title = gettext_lazy('Stwórz własny PDF')
562 submit = gettext_lazy('Pobierz')
563 template = 'catalogue/custom_pdf_form.html'
566 def __call__(self, *args, **kwargs):
567 if settings.NO_CUSTOM_PDF:
568 raise Http404('Custom PDF is disabled')
569 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
571 def form_args(self, request, obj):
572 """Override to parse view args and give additional args to the form."""
575 def validate_object(self, obj, request):
577 if not book.is_accessible_to(request.user):
578 return HttpResponseRedirect(book.get_absolute_url())
579 return super(CustomPDFFormView, self).validate_object(obj, request)
581 def get_object(self, request, slug, *args, **kwargs):
582 book = get_object_or_404(Book, slug=slug)
585 def context_description(self, request, obj):
586 return obj.pretty_title()
589 def tag_catalogue(request, category):
590 if category == 'theme':
591 tags = Tag.objects.usage_for_model(
592 Fragment, counts=True).filter(category='theme')
594 tags = list(get_top_level_related_tags((), categories=(category,)))
596 described_tags = [tag for tag in tags if tag.description]
598 if len(described_tags) > 4:
599 best = random.sample(described_tags, 4)
601 best = described_tags
603 template_name = 'catalogue/tag_catalogue.html'
604 return render(request, template_name, {
607 'title': constants.CATEGORIES_NAME_PLURAL[category],
608 'whole_category': constants.WHOLE_CATEGORY[category],
612 def collections(request):
613 objects = Collection.objects.filter(listed=True)
616 best = random.sample(list(objects), 4)
620 template_name = 'catalogue/collections.html'
621 return render(request, template_name, {
627 def ridero_cover(request, slug):
628 from librarian.cover import make_cover
629 wldoc = Book.objects.get(slug=slug).wldocument()
630 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
631 response = HttpResponse(content_type="image/png")
636 def get_isbn(request, book_format, slug):
637 book = Book.objects.get(slug=slug)
638 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))