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']
256 if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
257 self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
259 def get_queryset(self):
260 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
261 qs = qs.exclude(ancestor__in=qs)
263 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
264 if self.ctx['work_tags']:
266 Q(book__in=qs) | Q(book__ancestor__in=qs)
271 def get_suggested_tags(self, queryset):
272 tag_ids = [t.id for t in self.ctx['tags']]
275 current_books = self.get_queryset().values_list('book', flat=True).distinct()
276 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
278 related_tags.extend(list(
279 Tag.objects.usage_for_queryset(
281 ).exclude(category='set').exclude(pk__in=tag_ids)
283 if self.request.user.is_authenticated:
284 related_tags.extend(list(
285 Tag.objects.usage_for_queryset(
288 user=self.request.user
289 ).exclude(name='').exclude(pk__in=tag_ids)
292 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
293 if self.request.user.is_authenticated:
294 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
295 related_tags.extend(list(
296 Tag.objects.usage_for_queryset(
299 user=self.request.user
300 ).exclude(name='').exclude(pk__in=tag_ids)
303 fragments = Fragment.objects.filter(
304 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
307 Tag.objects.usage_for_queryset(
308 fragments, counts=True
309 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
310 .only('name', 'sort_key', 'category', 'slug'))
316 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
317 list_type='books', extra=None):
320 tag_ids = [tag.pk for tag in tags]
322 related_tag_lists = []
324 related_tag_lists.append(related_tags)
326 related_tag_lists.append(
327 Tag.objects.usage_for_queryset(
329 ).exclude(category='set').exclude(pk__in=tag_ids))
330 if request.user.is_authenticated:
331 related_tag_lists.append(
332 Tag.objects.usage_for_queryset(
336 ).exclude(name='').exclude(pk__in=tag_ids)
338 if not (extra and extra.get('theme_is_set')):
339 if fragments is None:
340 if list_type == 'gallery':
341 fragments = PictureArea.objects.filter(picture__in=objects)
343 fragments = Fragment.objects.filter(book__in=objects)
344 related_tag_lists.append(
345 Tag.objects.usage_for_queryset(
346 fragments, counts=True
347 ).filter(category='theme').exclude(pk__in=tag_ids)
348 .only('name', 'sort_key', 'category', 'slug'))
349 if isinstance(objects, QuerySet):
350 objects = prefetch_relations(objects, 'author')
353 categories = split_tags(*related_tag_lists)
355 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
356 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
358 objects = list(objects)
360 if not objects and len(tags) == 1 and list_type == 'books':
361 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
362 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
365 best = random.sample(objects, 3)
370 'object_list': objects,
371 'categories': categories,
373 'list_type': list_type,
375 'main_tag': tags[0] if tags else None,
377 'formats_form': forms.DownloadFormatsForm(),
379 'active_menu_item': list_type,
384 has_theme = any(((theme := x).category == 'theme' for x in tags))
386 result['main_tag'] = theme
387 template = 'catalogue/theme_detail.html'
389 template = 'catalogue/author_detail.html'
392 request, template, result,
396 class ResponseInstead(Exception):
397 def __init__(self, response):
398 super(ResponseInstead, self).__init__()
399 self.response = response
402 def analyse_tags(request, tag_str):
404 tags = Tag.get_tag_list(tag_str)
405 except Tag.DoesNotExist:
406 # Perhaps the user is asking about an author in Public Domain
407 # counter (they are not represented in tags)
408 chunks = tag_str.split('/')
409 if len(chunks) == 2 and chunks[0] == 'autor':
410 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
412 except Tag.MultipleObjectsReturned as e:
413 # Ask the user to disambiguate
414 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
415 except Tag.UrlDeprecationWarning as e:
416 raise ResponseInstead(HttpResponsePermanentRedirect(
417 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
422 if len(tags) > settings.MAX_TAG_LIST:
424 except AttributeError:
430 def theme_list(request, tags, list_type):
431 shelf_tags = [tag for tag in tags if tag.category == 'set']
432 fragment_tags = [tag for tag in tags if tag.category != 'set']
433 if list_type == 'gallery':
434 fragments = PictureArea.tagged.with_all(fragment_tags)
436 fragments = Fragment.tagged.with_all(fragment_tags)
439 # TODO: Pictures on shelves not supported yet.
440 books = Book.tagged.with_all(shelf_tags).order_by()
441 fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
442 elif list_type == 'books':
443 fragments = fragments.filter(book__findable=True)
445 if not fragments and len(tags) == 1 and list_type == 'books':
446 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
447 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
449 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
450 'theme_is_set': True,
451 'active_menu_item': 'theme',
455 def tagged_object_list(request, tags, list_type):
456 if list_type in ('books', 'audiobooks'):
457 return TaggedObjectList.as_view()(request, tags=tags)
460 tags = analyse_tags(request, tags)
461 except ResponseInstead as e:
464 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
467 if any(tag.category in ('theme', 'thing') for tag in tags):
468 return theme_list(request, tags, list_type=list_type)
470 if list_type == 'books':
471 books = Book.tagged.with_all(tags)
473 if any(tag.category == 'set' for tag in tags):
474 params = {'objects': books}
476 books = books.filter(findable=True)
478 'objects': Book.tagged_top_level(tags).filter(findable=True),
479 'fragments': Fragment.objects.filter(book__in=books),
480 'related_tags': get_top_level_related_tags(tags),
482 elif list_type == 'gallery':
483 params = {'objects': Picture.tagged.with_all(tags)}
484 elif list_type == 'audiobooks':
485 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
487 'objects': Book.tagged.with_all(tags, audiobooks),
489 'daisy': Book.tagged.with_all(
490 tags, audiobooks.filter(media__type='daisy').distinct()
497 return object_list(request, tags=tags, list_type=list_type, **params)
500 def book_fragments(request, slug, theme_slug):
501 book = get_object_or_404(Book, slug=slug)
502 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
503 fragments = Fragment.tagged.with_all([theme]).filter(
504 Q(book=book) | Q(book__ancestor=book))
506 template_name = 'catalogue/book_fragments.html'
513 'fragments': fragments,
514 'active_menu_item': 'books',
519 def book_detail(request, slug):
521 book = Book.objects.get(slug=slug)
522 except Book.DoesNotExist:
523 return pdcounter_views.book_stub_detail(request, slug)
527 'catalogue/book_detail.html',
530 'accessible': book.is_accessible_to(request.user),
531 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
532 'active_menu_item': 'books',
533 'club': Club.objects.first() if book.preview else None,
534 'donation_form': DonationStep1Form(),
538 def book_text(request, slug):
539 book = get_object_or_404(Book, slug=slug)
541 if not book.is_accessible_to(request.user):
542 return HttpResponseRedirect(book.get_absolute_url())
544 if not book.has_html_file():
546 with book.html_file.open('r') as f:
549 return render(request, 'catalogue/book_text.html', {
551 'extra_info': book.get_extra_info_json(),
552 'book_text': book_text,
553 'inserts': DynamicTextInsert.get_all(request),
555 'club': Club.objects.first(),
556 'donation_form': DonationStep1Form(),
565 def import_book(request):
566 """docstring for import_book"""
567 book_import_form = forms.BookImportForm(request.POST, request.FILES)
568 if book_import_form.is_valid():
570 book_import_form.save()
575 info = sys.exc_info()
576 exception = pprint.pformat(info[1])
577 tb = '\n'.join(traceback.format_tb(info[2]))
579 "Błąd: %(exception)s\n\n%(tb)s" % {
580 'exception': exception, 'tb': tb
582 content_type='text/plain'
584 return HttpResponse("Książka zaimportowana")
585 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
590 def book_info(request, book_id, lang='pl'):
591 book = get_object_or_404(Book, id=book_id)
592 # set language by hand
593 translation.activate(lang)
594 return render(request, 'catalogue/book_info.html', {'book': book})
597 def tag_info(request, tag_id):
598 tag = get_object_or_404(Tag, id=tag_id)
599 return HttpResponse(tag.description)
603 def embargo_link(request, key, format_, slug):
604 book = get_object_or_404(Book, slug=slug)
605 if format_ not in Book.formats:
607 if key != book.preview_key:
609 media_file = book.get_media(format_)
611 return HttpResponseRedirect(media_file.url)
612 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
615 def download_zip(request, file_format=None, media_format=None, slug=None):
617 url = Book.zip_format(file_format)
618 elif media_format and slug is not None:
619 book = get_object_or_404(Book, slug=slug)
620 url = book.zip_audiobooks(media_format)
622 raise Http404('No format specified for zip package')
623 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
626 class CustomPDFFormView(AjaxableFormView):
627 form_class = forms.CustomPDFForm
628 title = gettext_lazy('Stwórz własny PDF')
629 submit = gettext_lazy('Pobierz')
630 template = 'catalogue/custom_pdf_form.html'
633 def __call__(self, *args, **kwargs):
634 if settings.NO_CUSTOM_PDF:
635 raise Http404('Custom PDF is disabled')
636 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
638 def form_args(self, request, obj):
639 """Override to parse view args and give additional args to the form."""
642 def validate_object(self, obj, request):
644 if not book.is_accessible_to(request.user):
645 return HttpResponseRedirect(book.get_absolute_url())
646 return super(CustomPDFFormView, self).validate_object(obj, request)
648 def get_object(self, request, slug, *args, **kwargs):
649 book = get_object_or_404(Book, slug=slug)
652 def context_description(self, request, obj):
653 return obj.pretty_title()
656 def tag_catalogue(request, category):
657 if category == 'theme':
658 tags = Tag.objects.usage_for_model(
659 Fragment, counts=True).filter(category='theme')
661 tags = list(get_top_level_related_tags((), categories=(category,)))
663 described_tags = [tag for tag in tags if tag.description]
665 if len(described_tags) > 4:
666 best = random.sample(described_tags, 4)
668 best = described_tags
670 template_name = 'catalogue/tag_catalogue.html'
671 return render(request, template_name, {
674 'title': constants.CATEGORIES_NAME_PLURAL[category],
675 'whole_category': constants.WHOLE_CATEGORY[category],
676 'active_menu_item': 'theme' if category == 'theme' else None,
680 def collections(request):
681 objects = Collection.objects.filter(listed=True)
684 best = random.sample(list(objects), 4)
688 template_name = 'catalogue/collections.html'
689 return render(request, template_name, {
692 'active_menu_item': 'collections'
696 def ridero_cover(request, slug):
697 from librarian.cover import make_cover
698 wldoc = Book.objects.get(slug=slug).wldocument()
699 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
700 response = HttpResponse(content_type="image/png")
705 def get_isbn(request, book_format, slug):
706 book = Book.objects.get(slug=slug)
707 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))