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 'extra_info': book.get_extra_info_json(),
550 'book_text': book_text,
551 'inserts': DynamicTextInsert.get_all(request)
560 def import_book(request):
561 """docstring for import_book"""
562 book_import_form = forms.BookImportForm(request.POST, request.FILES)
563 if book_import_form.is_valid():
565 book_import_form.save()
570 info = sys.exc_info()
571 exception = pprint.pformat(info[1])
572 tb = '\n'.join(traceback.format_tb(info[2]))
574 "Błąd: %(exception)s\n\n%(tb)s" % {
575 'exception': exception, 'tb': tb
577 content_type='text/plain'
579 return HttpResponse("Książka zaimportowana")
580 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
585 def book_info(request, book_id, lang='pl'):
586 book = get_object_or_404(Book, id=book_id)
587 # set language by hand
588 translation.activate(lang)
589 return render(request, 'catalogue/book_info.html', {'book': book})
592 def tag_info(request, tag_id):
593 tag = get_object_or_404(Tag, id=tag_id)
594 return HttpResponse(tag.description)
598 def embargo_link(request, key, format_, slug):
599 book = get_object_or_404(Book, slug=slug)
600 if format_ not in Book.formats:
602 if key != book.preview_key:
604 media_file = book.get_media(format_)
606 return HttpResponseRedirect(media_file.url)
607 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
610 def download_zip(request, file_format=None, media_format=None, slug=None):
612 url = Book.zip_format(file_format)
613 elif media_format and slug is not None:
614 book = get_object_or_404(Book, slug=slug)
615 url = book.zip_audiobooks(media_format)
617 raise Http404('No format specified for zip package')
618 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
621 class CustomPDFFormView(AjaxableFormView):
622 form_class = forms.CustomPDFForm
623 title = gettext_lazy('Stwórz własny PDF')
624 submit = gettext_lazy('Pobierz')
625 template = 'catalogue/custom_pdf_form.html'
628 def __call__(self, *args, **kwargs):
629 if settings.NO_CUSTOM_PDF:
630 raise Http404('Custom PDF is disabled')
631 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
633 def form_args(self, request, obj):
634 """Override to parse view args and give additional args to the form."""
637 def validate_object(self, obj, request):
639 if not book.is_accessible_to(request.user):
640 return HttpResponseRedirect(book.get_absolute_url())
641 return super(CustomPDFFormView, self).validate_object(obj, request)
643 def get_object(self, request, slug, *args, **kwargs):
644 book = get_object_or_404(Book, slug=slug)
647 def context_description(self, request, obj):
648 return obj.pretty_title()
651 def tag_catalogue(request, category):
652 if category == 'theme':
653 tags = Tag.objects.usage_for_model(
654 Fragment, counts=True).filter(category='theme')
656 tags = list(get_top_level_related_tags((), categories=(category,)))
658 described_tags = [tag for tag in tags if tag.description]
660 if len(described_tags) > 4:
661 best = random.sample(described_tags, 4)
663 best = described_tags
665 template_name = 'catalogue/tag_catalogue.html'
666 return render(request, template_name, {
669 'title': constants.CATEGORIES_NAME_PLURAL[category],
670 'whole_category': constants.WHOLE_CATEGORY[category],
671 'active_menu_item': 'theme' if category == 'theme' else None,
675 def collections(request):
676 objects = Collection.objects.filter(listed=True)
679 best = random.sample(list(objects), 4)
683 template_name = 'catalogue/collections.html'
684 return render(request, template_name, {
687 'active_menu_item': 'collections'
691 def ridero_cover(request, slug):
692 from librarian.cover import make_cover
693 wldoc = Book.objects.get(slug=slug).wldocument()
694 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
695 response = HttpResponse(content_type="image/png")
700 def get_isbn(request, book_format, slug):
701 book = Book.objects.get(slug=slug)
702 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))