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),))
537 audiobook_list = list(iterate_audiobooks(book, ()))
540 for i, (file_path, lic, names) in enumerate(audiobook_list):
542 part_name = '_'.join(names)
544 part_name = '_' + part_name
545 ext = file_path.rsplit('.', 1)[-1]
546 if len(audiobook_list) > 1:
547 zip_name = f'{book.slug}_{index:03d}{part_name}'[:240] + '.' + ext
549 zip_name = book.slug[:240] + '.' + ext
550 zs.add_path(file_path, zip_name)
552 lic_name = constants.LICENSES.get(lic, {}).get('locative')
554 licenses.add(lic_name)
556 readme = render_to_string('catalogue/audiobook_zip_readme.txt', {
557 'licenses': licenses,
559 zs.add(readme, 'informacje.txt')
561 response = StreamingHttpResponse(zs, content_type='application/zip')
562 response['Content-Disposition'] = f'attachment; filename={slug}_{media_format}.zip'
566 class CustomPDFFormView(AjaxableFormView):
567 form_class = forms.CustomPDFForm
568 title = gettext_lazy('Stwórz własny PDF')
569 submit = gettext_lazy('Pobierz')
570 template = 'catalogue/custom_pdf_form.html'
573 def __call__(self, *args, **kwargs):
574 if settings.NO_CUSTOM_PDF:
575 raise Http404('Custom PDF is disabled')
576 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
578 def form_args(self, request, obj):
579 """Override to parse view args and give additional args to the form."""
582 def validate_object(self, obj, request):
584 if not book.is_accessible_to(request.user):
585 return HttpResponseRedirect(book.get_absolute_url())
586 return super(CustomPDFFormView, self).validate_object(obj, request)
588 def get_object(self, request, slug, *args, **kwargs):
589 book = get_object_or_404(Book, slug=slug)
592 def context_description(self, request, obj):
593 return obj.pretty_title()
596 def tag_catalogue(request, category):
597 if category == 'theme':
598 tags = Tag.objects.usage_for_model(
599 Fragment, counts=True).filter(category='theme')
601 tags = list(get_top_level_related_tags((), categories=(category,)))
603 described_tags = [tag for tag in tags if tag.description]
605 if len(described_tags) > 4:
606 best = random.sample(described_tags, 4)
608 best = described_tags
610 template_name = 'catalogue/tag_catalogue.html'
611 return render(request, template_name, {
614 'title': constants.CATEGORIES_NAME_PLURAL[category],
615 'whole_category': constants.WHOLE_CATEGORY[category],
619 def collections(request):
620 objects = Collection.objects.filter(listed=True)
623 best = random.sample(list(objects), 4)
627 template_name = 'catalogue/collections.html'
628 return render(request, template_name, {
634 def ridero_cover(request, slug):
635 from librarian.cover import make_cover
636 wldoc = Book.objects.get(slug=slug).wldocument()
637 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
638 response = HttpResponse(content_type="image/png")
643 def get_isbn(request, book_format, slug):
644 book = Book.objects.get(slug=slug)
645 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))