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 wolnelektury.utils import is_ajax
27 from catalogue import constants
28 from catalogue import forms
29 from catalogue.helpers import get_top_level_related_tags
30 from catalogue.models import Book, Collection, Tag, Fragment
31 from catalogue.models.tag import TagRelation
32 from catalogue.utils import split_tags
33 from catalogue.models.tag import prefetch_relations
35 staff_required = user_passes_test(lambda user: user.is_staff)
38 def catalogue(request):
39 return render(request, 'catalogue/catalogue.html', {
40 'books': Book.objects.filter(findable=True, parent=None),
41 'collections': Collection.objects.filter(listed=True),
42 'active_menu_item': 'all_works',
46 def daisy_list(request):
47 return object_list(request, Book.objects.filter(media__type='daisy'))
50 def collection(request, slug):
51 coll = get_object_or_404(Collection, slug=slug)
52 template_name = 'catalogue/collection.html'
53 return render(request, template_name, {
55 'active_menu_item': 'collections',
59 def differentiate_tags(request, tags, ambiguous_slugs):
60 beginning = '/'.join(tag.url_chunk for tag in tags)
61 unparsed = '/'.join(ambiguous_slugs[1:])
63 for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
65 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
70 'catalogue/differentiate_tags.html',
71 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
75 from django.db.models import FilteredRelation, Q
76 from django.views.decorators.cache import cache_control
77 from django.views.decorators.vary import vary_on_headers
78 from django.utils.decorators import method_decorator
82 vary_on_headers('X-Requested-With'),
83 cache_control(max_age=1),
85 class ObjectListView(TemplateView):
88 item_template_name = ''
90 default_ordering = None
93 self.is_themed = False
97 def dispatch(self, *args, **kwargs):
100 except ResponseInstead as e:
102 return super().dispatch(*args, **kwargs)
104 def get_orderings(self):
105 order = self.get_order()
110 "active": k == order,
111 "default": v[0] is None,
113 for k, v in self.orderings.items()
117 order = self.request.GET.get('order')
118 if order not in self.orderings:
119 order = self.default_ordering
123 order_tag = self.get_order()
125 order = self.orderings[order_tag]
128 qs = qs.order_by(order_by)
131 def search(self, qs):
134 def get_template_names(self):
135 if is_ajax(self.request) or self.request.GET.get('dyn'):
137 return self.dynamic_themed_template_name
139 return self.dynamic_template_name
142 return self.themed_template_name
144 return self.template_name
146 def get_context_data(self, **kwargs):
147 ctx = super().get_context_data()
149 qs = self.get_queryset()
153 ctx['object_list'] = qs
154 ctx['suggested_tags'] = self.get_suggested_tags(qs)
155 ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
159 class BookList(ObjectListView):
160 title = gettext_lazy('Literatura')
162 template_name = 'catalogue/book_list.html'
163 dynamic_template_name = 'catalogue/dynamic_book_list.html'
164 themed_template_name = 'catalogue/themed_book_list.html'
165 dynamic_themed_template_name = 'catalogue/dynamic_themed_book_list.html'
168 'pop': ('-popularity__count', gettext_lazy('najpopularniejsze')),
169 'alpha': (None, gettext_lazy('alfabetycznie')),
171 default_ordering = 'alpha'
173 def get_queryset(self):
174 return Book.objects.filter(parent=None, findable=True)
176 def search(self, qs):
177 term = self.request.GET.get('search')
179 meta_rels = TagRelation.objects.filter(tag__category='author')
180 # TODO: search tags in currently displaying language
182 rels = meta_rels.filter(tag__name_pl__icontains=term)
184 Q(book__title__icontains=term) |
185 Q(tag_relations__in=rels) |
186 Q(text__icontains=term)
190 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
192 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
196 class LiteratureView(BookList):
197 def get_suggested_tags(self, queryset):
198 tags = list(get_top_level_related_tags([]))
199 tags.sort(key=lambda t: -t.count)
200 if self.request.user.is_authenticated:
201 tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
205 class AudiobooksView(LiteratureView):
206 title = gettext_lazy('Audiobooki')
207 list_type = 'audiobooks'
209 def get_queryset(self):
210 return Book.objects.filter(findable=True, media__type='mp3').distinct()
213 class TaggedObjectList(BookList):
216 self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
217 self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
218 self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
219 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
221 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
222 elif self.ctx['tags']:
223 self.ctx['main_tag'] = self.ctx['tags'][0]
225 self.ctx['main_tag'] = None
226 self.ctx['filtering_tags'] = [
227 t for t in self.ctx['tags']
228 if t is not self.ctx['main_tag']
230 if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
231 self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
233 def get_queryset(self):
234 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
235 qs = qs.exclude(ancestor__in=qs)
237 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
238 if self.ctx['work_tags']:
240 Q(book__in=qs) | Q(book__ancestor__in=qs)
245 def get_suggested_tags(self, queryset):
246 tag_ids = [t.id for t in self.ctx['tags']]
249 current_books = self.get_queryset().values_list('book', flat=True).distinct()
250 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
252 related_tags.extend(list(
253 Tag.objects.usage_for_queryset(
255 ).exclude(category='set').exclude(pk__in=tag_ids)
257 if self.request.user.is_authenticated:
258 related_tags.extend(list(
259 Tag.objects.usage_for_queryset(
262 user=self.request.user
263 ).exclude(name='').exclude(pk__in=tag_ids)
266 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
267 if self.request.user.is_authenticated:
268 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
269 related_tags.extend(list(
270 Tag.objects.usage_for_queryset(
273 user=self.request.user
274 ).exclude(name='').exclude(pk__in=tag_ids)
277 fragments = Fragment.objects.filter(
278 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
281 Tag.objects.usage_for_queryset(
282 fragments, counts=True
283 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
284 .only('name', 'sort_key', 'category', 'slug'))
290 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
291 list_type='books', extra=None):
294 tag_ids = [tag.pk for tag in tags]
296 related_tag_lists = []
298 related_tag_lists.append(related_tags)
300 related_tag_lists.append(
301 Tag.objects.usage_for_queryset(
303 ).exclude(category='set').exclude(pk__in=tag_ids))
304 if request.user.is_authenticated:
305 related_tag_lists.append(
306 Tag.objects.usage_for_queryset(
310 ).exclude(name='').exclude(pk__in=tag_ids)
312 if not (extra and extra.get('theme_is_set')):
313 if fragments is None:
314 if list_type == 'gallery':
315 fragments = PictureArea.objects.filter(picture__in=objects)
317 fragments = Fragment.objects.filter(book__in=objects)
318 related_tag_lists.append(
319 Tag.objects.usage_for_queryset(
320 fragments, counts=True
321 ).filter(category='theme').exclude(pk__in=tag_ids)
322 .only('name', 'sort_key', 'category', 'slug'))
323 if isinstance(objects, QuerySet):
324 objects = prefetch_relations(objects, 'author')
327 categories = split_tags(*related_tag_lists)
329 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
330 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
332 objects = list(objects)
334 if not objects and len(tags) == 1 and list_type == 'books':
335 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
336 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
339 best = random.sample(objects, 3)
344 'object_list': objects,
345 'categories': categories,
347 'list_type': list_type,
349 'main_tag': tags[0] if tags else None,
351 'formats_form': forms.DownloadFormatsForm(),
353 'active_menu_item': list_type,
358 has_theme = any(((theme := x).category == 'theme' for x in tags))
360 result['main_tag'] = theme
361 template = 'catalogue/theme_detail.html'
363 template = 'catalogue/author_detail.html'
366 request, template, result,
370 class ResponseInstead(Exception):
371 def __init__(self, response):
372 super(ResponseInstead, self).__init__()
373 self.response = response
376 def analyse_tags(request, tag_str):
378 tags = Tag.get_tag_list(tag_str)
379 except Tag.DoesNotExist:
380 # Perhaps the user is asking about an author in Public Domain
381 # counter (they are not represented in tags)
382 chunks = tag_str.split('/')
383 if len(chunks) == 2 and chunks[0] == 'autor':
384 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
386 except Tag.MultipleObjectsReturned as e:
387 # Ask the user to disambiguate
388 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
389 except Tag.UrlDeprecationWarning as e:
390 raise ResponseInstead(HttpResponsePermanentRedirect(
391 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
396 if len(tags) > settings.MAX_TAG_LIST:
398 except AttributeError:
404 def theme_list(request, tags, list_type):
405 shelf_tags = [tag for tag in tags if tag.category == 'set']
406 fragment_tags = [tag for tag in tags if tag.category != 'set']
407 if list_type == 'gallery':
408 fragments = PictureArea.tagged.with_all(fragment_tags)
410 fragments = Fragment.tagged.with_all(fragment_tags)
413 # TODO: Pictures on shelves not supported yet.
414 books = Book.tagged.with_all(shelf_tags).order_by()
415 fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
416 elif list_type == 'books':
417 fragments = fragments.filter(book__findable=True)
419 if not fragments and len(tags) == 1 and list_type == 'books':
420 if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
421 return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
423 return object_list(request, fragments, tags=tags, list_type=list_type, extra={
424 'theme_is_set': True,
425 'active_menu_item': 'theme',
429 def tagged_object_list(request, tags, list_type):
430 if list_type in ('books', 'audiobooks'):
431 return TaggedObjectList.as_view()(request, tags=tags)
434 tags = analyse_tags(request, tags)
435 except ResponseInstead as e:
438 if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
441 if any(tag.category in ('theme', 'thing') for tag in tags):
442 return theme_list(request, tags, list_type=list_type)
444 if list_type == 'books':
445 books = Book.tagged.with_all(tags)
447 if any(tag.category == 'set' for tag in tags):
448 params = {'objects': books}
450 books = books.filter(findable=True)
452 'objects': Book.tagged_top_level(tags).filter(findable=True),
453 'fragments': Fragment.objects.filter(book__in=books),
454 'related_tags': get_top_level_related_tags(tags),
456 elif list_type == 'gallery':
457 params = {'objects': Picture.tagged.with_all(tags)}
458 elif list_type == 'audiobooks':
459 audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
461 'objects': Book.tagged.with_all(tags, audiobooks),
463 'daisy': Book.tagged.with_all(
464 tags, audiobooks.filter(media__type='daisy').distinct()
471 return object_list(request, tags=tags, list_type=list_type, **params)
474 def book_fragments(request, slug, theme_slug):
475 book = get_object_or_404(Book, slug=slug)
476 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
477 fragments = Fragment.tagged.with_all([theme]).filter(
478 Q(book=book) | Q(book__ancestor=book))
480 template_name = 'catalogue/book_fragments.html'
487 'fragments': fragments,
488 'active_menu_item': 'books',
493 def book_detail(request, slug):
495 book = Book.objects.get(slug=slug)
496 except Book.DoesNotExist:
497 return pdcounter_views.book_stub_detail(request, slug)
501 'catalogue/book_detail.html',
504 'accessible': book.is_accessible_to(request.user),
505 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
506 'active_menu_item': 'books',
507 'club': Club.objects.first() if book.preview else None,
508 'donation_form': DonationStep1Form(),
512 def book_text(request, slug):
513 book = get_object_or_404(Book, slug=slug)
515 if not book.is_accessible_to(request.user):
516 return HttpResponseRedirect(book.get_absolute_url())
518 if not book.has_html_file():
520 with book.html_file.open('r') as f:
523 return render(request, 'catalogue/book_text.html', {
525 'extra_info': book.get_extra_info_json(),
526 'book_text': book_text,
527 'inserts': DynamicTextInsert.get_all(request),
529 'club': Club.objects.first(),
530 'donation_form': DonationStep1Form(),
539 def import_book(request):
540 """docstring for import_book"""
541 book_import_form = forms.BookImportForm(request.POST, request.FILES)
542 if book_import_form.is_valid():
544 book_import_form.save()
549 info = sys.exc_info()
550 exception = pprint.pformat(info[1])
551 tb = '\n'.join(traceback.format_tb(info[2]))
553 "Błąd: %(exception)s\n\n%(tb)s" % {
554 'exception': exception, 'tb': tb
556 content_type='text/plain'
558 return HttpResponse("Książka zaimportowana")
559 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
564 def book_info(request, book_id, lang='pl'):
565 book = get_object_or_404(Book, id=book_id)
566 # set language by hand
567 translation.activate(lang)
568 return render(request, 'catalogue/book_info.html', {'book': book})
571 def tag_info(request, tag_id):
572 tag = get_object_or_404(Tag, id=tag_id)
573 return HttpResponse(tag.description)
577 def embargo_link(request, key, format_, slug):
578 book = get_object_or_404(Book, slug=slug)
579 if format_ not in Book.formats:
581 if key != book.preview_key:
583 media_file = book.get_media(format_)
585 return HttpResponseRedirect(media_file.url)
586 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
589 def download_zip(request, file_format=None, media_format=None, slug=None):
591 url = Book.zip_format(file_format)
592 elif media_format and slug is not None:
593 book = get_object_or_404(Book, slug=slug)
594 url = book.zip_audiobooks(media_format)
596 raise Http404('No format specified for zip package')
597 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
600 class CustomPDFFormView(AjaxableFormView):
601 form_class = forms.CustomPDFForm
602 title = gettext_lazy('Stwórz własny PDF')
603 submit = gettext_lazy('Pobierz')
604 template = 'catalogue/custom_pdf_form.html'
607 def __call__(self, *args, **kwargs):
608 if settings.NO_CUSTOM_PDF:
609 raise Http404('Custom PDF is disabled')
610 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
612 def form_args(self, request, obj):
613 """Override to parse view args and give additional args to the form."""
616 def validate_object(self, obj, request):
618 if not book.is_accessible_to(request.user):
619 return HttpResponseRedirect(book.get_absolute_url())
620 return super(CustomPDFFormView, self).validate_object(obj, request)
622 def get_object(self, request, slug, *args, **kwargs):
623 book = get_object_or_404(Book, slug=slug)
626 def context_description(self, request, obj):
627 return obj.pretty_title()
630 def tag_catalogue(request, category):
631 if category == 'theme':
632 tags = Tag.objects.usage_for_model(
633 Fragment, counts=True).filter(category='theme')
635 tags = list(get_top_level_related_tags((), categories=(category,)))
637 described_tags = [tag for tag in tags if tag.description]
639 if len(described_tags) > 4:
640 best = random.sample(described_tags, 4)
642 best = described_tags
644 template_name = 'catalogue/tag_catalogue.html'
645 return render(request, template_name, {
648 'title': constants.CATEGORIES_NAME_PLURAL[category],
649 'whole_category': constants.WHOLE_CATEGORY[category],
650 'active_menu_item': 'theme' if category == 'theme' else None,
654 def collections(request):
655 objects = Collection.objects.filter(listed=True)
658 best = random.sample(list(objects), 4)
662 template_name = 'catalogue/collections.html'
663 return render(request, template_name, {
666 'active_menu_item': 'collections'
670 def ridero_cover(request, slug):
671 from librarian.cover import make_cover
672 wldoc = Book.objects.get(slug=slug).wldocument()
673 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
674 response = HttpResponse(content_type="image/png")
679 def get_isbn(request, book_format, slug):
680 book = Book.objects.get(slug=slug)
681 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))