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
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_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 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 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, {
57 'active_menu_item': 'collections',
61 def differentiate_tags(request, tags, ambiguous_slugs):
62 beginning = '/'.join(tag.url_chunk for tag in tags)
63 unparsed = '/'.join(ambiguous_slugs[1:])
65 for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
67 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
72 'catalogue/differentiate_tags.html',
73 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
77 from django.db.models import FilteredRelation, Q
78 from django.views.decorators.cache import cache_control
79 from django.views.decorators.vary import vary_on_headers
80 from django.utils.decorators import method_decorator
84 vary_on_headers('X-Requested-With'),
85 cache_control(max_age=1),
87 class ObjectListView(TemplateView):
90 item_template_name = ''
92 default_ordering = None
95 self.is_themed = False
99 def dispatch(self, *args, **kwargs):
102 except ResponseInstead as e:
104 return super().dispatch(*args, **kwargs)
106 def get_orderings(self):
107 order = self.get_order()
112 "active": k == order,
113 "default": v[0] is None,
115 for k, v in self.orderings.items()
119 order = self.request.GET.get('order')
120 if order not in self.orderings:
121 order = self.default_ordering
125 order_tag = self.get_order()
127 order = self.orderings[order_tag]
130 qs = qs.order_by(order_by)
133 def search(self, qs):
136 def get_template_names(self):
137 if is_ajax(self.request) or self.request.GET.get('dyn'):
139 return self.dynamic_themed_template_name
141 return self.dynamic_template_name
144 return self.themed_template_name
146 return self.template_name
148 def get_context_data(self, **kwargs):
149 ctx = super().get_context_data()
151 qs = self.get_queryset()
155 ctx['object_list'] = qs
156 ctx['suggested_tags'] = self.get_suggested_tags(qs)
157 ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
161 class BookList(ObjectListView):
162 title = gettext_lazy('Literatura')
164 template_name = 'catalogue/book_list.html'
165 dynamic_template_name = 'catalogue/dynamic_book_list.html'
166 themed_template_name = 'catalogue/themed_book_list.html'
167 dynamic_themed_template_name = 'catalogue/dynamic_themed_book_list.html'
170 'pop': ('-popularity__count', gettext_lazy('najpopularniejsze')),
171 'alpha': (None, gettext_lazy('alfabetycznie')),
173 default_ordering = 'alpha'
175 def get_queryset(self):
176 return Book.objects.filter(parent=None, findable=True)
178 def search(self, qs):
179 term = self.request.GET.get('search')
181 meta_rels = TagRelation.objects.filter(tag__category='author')
182 # TODO: search tags in currently displaying language
184 rels = meta_rels.filter(tag__name_pl__icontains=term)
186 Q(book__title__icontains=term) |
187 Q(tag_relations__in=rels) |
188 Q(text__icontains=term)
192 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
194 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
198 class ArtList(ObjectListView):
199 template_name = 'catalogue/book_list.html'
200 dynamic_template_name = 'catalogue/dynamic_book_list.html'
201 title = gettext_lazy('Sztuka')
202 list_type = 'gallery'
204 def get_queryset(self):
205 return Picture.objects.all()
207 def search(self, qs):
208 term = self.request.GET.get('search')
210 qs = qs.filter(Q(title__icontains=term) | Q(tag_relations__tag__name_pl__icontains=term)).distinct()
214 class LiteratureView(BookList):
215 def get_suggested_tags(self, queryset):
216 tags = list(get_top_level_related_tags([]))
217 tags.sort(key=lambda t: -t.count)
218 if self.request.user.is_authenticated:
219 tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
223 class AudiobooksView(LiteratureView):
224 title = gettext_lazy('Audiobooki')
225 list_type = 'audiobooks'
227 def get_queryset(self):
228 return Book.objects.filter(findable=True, media__type='mp3').distinct()
231 class GalleryView(ArtList):
232 def get_suggested_tags(self, queryset):
233 return Tag.objects.usage_for_queryset(
236 ).exclude(pk__in=[t.id for t in self.ctx['tags']]).order_by('-count')
239 class TaggedObjectList(BookList):
242 self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
243 self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
244 self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
245 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
247 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
248 elif self.ctx['tags']:
249 self.ctx['main_tag'] = self.ctx['tags'][0]
251 self.ctx['main_tag'] = None
252 self.ctx['filtering_tags'] = [
253 t for t in self.ctx['tags']
254 if t is not self.ctx['main_tag']
257 def get_queryset(self):
258 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
259 qs = qs.exclude(ancestor__in=qs)
261 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
262 if self.ctx['work_tags']:
264 Q(book__in=qs) | Q(book__ancestor__in=qs)
269 def get_suggested_tags(self, queryset):
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 if self.request.user.is_authenticated:
282 related_tags.extend(list(
283 Tag.objects.usage_for_queryset(
286 user=self.request.user
287 ).exclude(name='').exclude(pk__in=tag_ids)
290 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
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 has_theme = any(((theme := x).category == 'theme' for x in tags))
384 result['main_tag'] = theme
385 template = 'catalogue/theme_detail.html'
387 template = 'catalogue/author_detail.html'
390 request, template, result,
394 class ResponseInstead(Exception):
395 def __init__(self, response):
396 super(ResponseInstead, self).__init__()
397 self.response = response
400 def analyse_tags(request, tag_str):
402 tags = Tag.get_tag_list(tag_str)
403 except Tag.DoesNotExist:
404 # Perhaps the user is asking about an author in Public Domain
405 # counter (they are not represented in tags)
406 chunks = tag_str.split('/')
407 if len(chunks) == 2 and chunks[0] == 'autor':
408 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
410 except Tag.MultipleObjectsReturned as e:
411 # Ask the user to disambiguate
412 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
413 except Tag.UrlDeprecationWarning as e:
414 raise ResponseInstead(HttpResponsePermanentRedirect(
415 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
420 if len(tags) > settings.MAX_TAG_LIST:
422 except AttributeError:
428 def theme_list(request, tags, list_type):
429 shelf_tags = [tag for tag in tags if tag.category == 'set']
430 fragment_tags = [tag for tag in tags if tag.category != 'set']
431 if list_type == 'gallery':
432 fragments = PictureArea.tagged.with_all(fragment_tags)
434 fragments = Fragment.tagged.with_all(fragment_tags)
437 # TODO: Pictures on shelves not supported yet.
438 books = Book.tagged.with_all(shelf_tags).order_by()
439 fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
440 elif list_type == 'books':
441 fragments = fragments.filter(book__findable=True)
443 if not fragments and len(tags) == 1 and list_type == 'books':
444 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
445 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
447 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
448 'theme_is_set': True,
449 'active_menu_item': 'theme',
453 def tagged_object_list(request, tags, list_type):
454 if list_type in ('books', 'audiobooks'):
455 return TaggedObjectList.as_view()(request, tags=tags)
458 tags = analyse_tags(request, tags)
459 except ResponseInstead as e:
462 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
465 if any(tag.category in ('theme', 'thing') for tag in tags):
466 return theme_list(request, tags, list_type=list_type)
468 if list_type == 'books':
469 books = Book.tagged.with_all(tags)
471 if any(tag.category == 'set' for tag in tags):
472 params = {'objects': books}
474 books = books.filter(findable=True)
476 'objects': Book.tagged_top_level(tags).filter(findable=True),
477 'fragments': Fragment.objects.filter(book__in=books),
478 'related_tags': get_top_level_related_tags(tags),
480 elif list_type == 'gallery':
481 params = {'objects': Picture.tagged.with_all(tags)}
482 elif list_type == 'audiobooks':
483 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
485 'objects': Book.tagged.with_all(tags, audiobooks),
487 'daisy': Book.tagged.with_all(
488 tags, audiobooks.filter(media__type='daisy').distinct()
495 return object_list(request, tags=tags, list_type=list_type, **params)
498 def book_fragments(request, slug, theme_slug):
499 book = get_object_or_404(Book, slug=slug)
500 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
501 fragments = Fragment.tagged.with_all([theme]).filter(
502 Q(book=book) | Q(book__ancestor=book))
504 template_name = 'catalogue/book_fragments.html'
511 'fragments': fragments,
512 'active_menu_item': 'books',
517 def book_detail(request, slug):
519 book = Book.objects.get(slug=slug)
520 except Book.DoesNotExist:
521 return pdcounter_views.book_stub_detail(request, slug)
525 'catalogue/book_detail.html',
528 'accessible': book.is_accessible_to(request.user),
529 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
530 'active_menu_item': 'books',
531 'club': Club.objects.first() if book.preview else None,
532 'donation_form': DonationStep1Form(),
536 def book_text(request, slug):
537 book = get_object_or_404(Book, slug=slug)
539 if not book.is_accessible_to(request.user):
540 return HttpResponseRedirect(book.get_absolute_url())
542 if not book.has_html_file():
544 with book.html_file.open('r') as f:
547 return render(request, 'catalogue/book_text.html', {
549 'book_text': book_text,
550 'inserts': DynamicTextInsert.get_all(request)
559 def import_book(request):
560 """docstring for import_book"""
561 book_import_form = forms.BookImportForm(request.POST, request.FILES)
562 if book_import_form.is_valid():
564 book_import_form.save()
569 info = sys.exc_info()
570 exception = pprint.pformat(info[1])
571 tb = '\n'.join(traceback.format_tb(info[2]))
573 "Błąd: %(exception)s\n\n%(tb)s" % {
574 'exception': exception, 'tb': tb
576 content_type='text/plain'
578 return HttpResponse("Książka zaimportowana")
579 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
584 def book_info(request, book_id, lang='pl'):
585 book = get_object_or_404(Book, id=book_id)
586 # set language by hand
587 translation.activate(lang)
588 return render(request, 'catalogue/book_info.html', {'book': book})
591 def tag_info(request, tag_id):
592 tag = get_object_or_404(Tag, id=tag_id)
593 return HttpResponse(tag.description)
597 def embargo_link(request, key, format_, slug):
598 book = get_object_or_404(Book, slug=slug)
599 if format_ not in Book.formats:
601 if key != book.preview_key:
603 media_file = book.get_media(format_)
605 return HttpResponseRedirect(media_file.url)
606 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
609 def download_zip(request, file_format=None, media_format=None, slug=None):
611 url = Book.zip_format(file_format)
612 elif media_format and slug is not None:
613 book = get_object_or_404(Book, slug=slug)
614 url = book.zip_audiobooks(media_format)
616 raise Http404('No format specified for zip package')
617 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
620 class CustomPDFFormView(AjaxableFormView):
621 form_class = forms.CustomPDFForm
622 title = gettext_lazy('Stwórz własny PDF')
623 submit = gettext_lazy('Pobierz')
624 template = 'catalogue/custom_pdf_form.html'
627 def __call__(self, *args, **kwargs):
628 if settings.NO_CUSTOM_PDF:
629 raise Http404('Custom PDF is disabled')
630 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
632 def form_args(self, request, obj):
633 """Override to parse view args and give additional args to the form."""
636 def validate_object(self, obj, request):
638 if not book.is_accessible_to(request.user):
639 return HttpResponseRedirect(book.get_absolute_url())
640 return super(CustomPDFFormView, self).validate_object(obj, request)
642 def get_object(self, request, slug, *args, **kwargs):
643 book = get_object_or_404(Book, slug=slug)
646 def context_description(self, request, obj):
647 return obj.pretty_title()
650 def tag_catalogue(request, category):
651 if category == 'theme':
652 tags = Tag.objects.usage_for_model(
653 Fragment, counts=True).filter(category='theme')
655 tags = list(get_top_level_related_tags((), categories=(category,)))
657 described_tags = [tag for tag in tags if tag.description]
659 if len(described_tags) > 4:
660 best = random.sample(described_tags, 4)
662 best = described_tags
664 template_name = 'catalogue/tag_catalogue.html'
665 return render(request, template_name, {
668 'title': constants.CATEGORIES_NAME_PLURAL[category],
669 'whole_category': constants.WHOLE_CATEGORY[category],
670 'active_menu_item': 'theme' if category == 'theme' else None,
674 def collections(request):
675 objects = Collection.objects.filter(listed=True)
678 best = random.sample(list(objects), 4)
682 template_name = 'catalogue/collections.html'
683 return render(request, template_name, {
686 'active_menu_item': 'collections'
690 def ridero_cover(request, slug):
691 from librarian.cover import make_cover
692 wldoc = Book.objects.get(slug=slug).wldocument()
693 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
694 response = HttpResponse(content_type="image/png")
699 def get_isbn(request, book_format, slug):
700 book = Book.objects.get(slug=slug)
701 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))