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),
45 def daisy_list(request):
46 return object_list(request, Book.objects.filter(media__type='daisy'))
49 def collection(request, slug):
50 coll = get_object_or_404(Collection, slug=slug)
51 template_name = 'catalogue/collection.html'
52 return render(request, template_name, {
57 def differentiate_tags(request, tags, ambiguous_slugs):
58 beginning = '/'.join(tag.url_chunk for tag in tags)
59 unparsed = '/'.join(ambiguous_slugs[1:])
61 for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
63 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
68 'catalogue/differentiate_tags.html',
69 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
73 from django.db.models import FilteredRelation, Q
74 from django.views.decorators.cache import cache_control
75 from django.views.decorators.vary import vary_on_headers
76 from django.utils.decorators import method_decorator
80 vary_on_headers('X-Requested-With'),
81 cache_control(max_age=1),
83 class ObjectListView(TemplateView):
86 item_template_name = ''
88 default_ordering = None
91 self.is_themed = False
95 def dispatch(self, *args, **kwargs):
98 except ResponseInstead as e:
100 return super().dispatch(*args, **kwargs)
102 def get_orderings(self):
103 order = self.get_order()
108 "active": k == order,
109 "default": v[0] is None,
111 for k, v in self.orderings.items()
115 order = self.request.GET.get('order')
116 if order not in self.orderings:
117 order = self.default_ordering
121 order_tag = self.get_order()
123 order = self.orderings[order_tag]
126 qs = qs.order_by(order_by)
129 def search(self, qs):
132 def get_template_names(self):
133 if is_ajax(self.request) or self.request.GET.get('dyn'):
135 return self.dynamic_themed_template_name
137 return self.dynamic_template_name
140 return self.themed_template_name
142 return self.template_name
144 def get_context_data(self, **kwargs):
145 ctx = super().get_context_data()
147 qs = self.get_queryset()
151 ctx['object_list'] = qs
152 ctx['suggested_tags'] = self.get_suggested_tags(qs)
153 ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
157 class BookList(ObjectListView):
158 title = gettext_lazy('Literatura')
160 template_name = 'catalogue/book_list.html'
161 dynamic_template_name = 'catalogue/dynamic_book_list.html'
162 themed_template_name = 'catalogue/themed_book_list.html'
163 dynamic_themed_template_name = 'catalogue/dynamic_themed_book_list.html'
166 'pop': ('-popularity__count', gettext_lazy('najpopularniejsze')),
167 'alpha': (None, gettext_lazy('alfabetycznie')),
169 default_ordering = 'alpha'
171 def get_queryset(self):
172 return Book.objects.filter(parent=None, findable=True)
174 def search(self, qs):
175 term = self.request.GET.get('search')
177 meta_rels = TagRelation.objects.filter(tag__category='author')
178 # TODO: search tags in currently displaying language
180 rels = meta_rels.filter(tag__name_pl__icontains=term)
182 Q(book__title__icontains=term) |
183 Q(tag_relations__in=rels) |
184 Q(text__icontains=term)
188 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
190 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
194 class LiteratureView(BookList):
195 def get_suggested_tags(self, queryset):
196 tags = list(get_top_level_related_tags([]))
197 tags.sort(key=lambda t: -t.count)
198 if self.request.user.is_authenticated:
199 tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
203 class AudiobooksView(LiteratureView):
204 title = gettext_lazy('Audiobooki')
205 list_type = 'audiobooks'
207 def get_queryset(self):
208 return Book.objects.filter(findable=True, media__type='mp3').distinct()
211 class TaggedObjectList(BookList):
214 self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
215 self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
216 self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
217 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
219 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
220 elif self.ctx['tags']:
221 self.ctx['main_tag'] = self.ctx['tags'][0]
223 self.ctx['main_tag'] = None
224 self.ctx['filtering_tags'] = [
225 t for t in self.ctx['tags']
226 if t is not self.ctx['main_tag']
228 if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
229 self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
230 self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
232 def get_queryset(self):
233 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
234 qs = qs.exclude(ancestor__in=qs)
236 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
237 if self.ctx['work_tags']:
239 Q(book__in=qs) | Q(book__ancestor__in=qs)
244 def get_suggested_tags(self, queryset):
245 tag_ids = [t.id for t in self.ctx['tags']]
248 current_books = self.get_queryset().values_list('book', flat=True).distinct()
249 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
251 related_tags.extend(list(
252 Tag.objects.usage_for_queryset(
254 ).exclude(category='set').exclude(pk__in=tag_ids)
256 if self.request.user.is_authenticated:
257 related_tags.extend(list(
258 Tag.objects.usage_for_queryset(
261 user=self.request.user
262 ).exclude(name='').exclude(pk__in=tag_ids)
265 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
266 if self.request.user.is_authenticated:
267 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
268 related_tags.extend(list(
269 Tag.objects.usage_for_queryset(
272 user=self.request.user
273 ).exclude(name='').exclude(pk__in=tag_ids)
276 fragments = Fragment.objects.filter(
277 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
280 Tag.objects.usage_for_queryset(
281 fragments, counts=True
282 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
283 .only('name', 'sort_key', 'category', 'slug'))
289 def object_list(request, objects, list_type='books'):
290 related_tag_lists = []
292 related_tag_lists.append(
293 Tag.objects.usage_for_queryset(
295 ).exclude(category='set'))
296 if request.user.is_authenticated:
297 related_tag_lists.append(
298 Tag.objects.usage_for_queryset(
305 fragments = Fragment.objects.filter(book__in=objects)
306 related_tag_lists.append(
307 Tag.objects.usage_for_queryset(
308 fragments, counts=True
309 ).filter(category='theme')
310 .only('name', 'sort_key', 'category', 'slug'))
311 if isinstance(objects, QuerySet):
312 objects = prefetch_relations(objects, 'author')
314 categories = split_tags(*related_tag_lists)
316 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
317 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
319 objects = list(objects)
322 'object_list': objects,
324 'list_type': list_type,
327 template = 'catalogue/author_detail.html'
330 request, template, result,
334 class ResponseInstead(Exception):
335 def __init__(self, response):
336 super(ResponseInstead, self).__init__()
337 self.response = response
340 def analyse_tags(request, tag_str):
342 tags = Tag.get_tag_list(tag_str)
343 except Tag.DoesNotExist:
344 # Perhaps the user is asking about an author in Public Domain
345 # counter (they are not represented in tags)
346 chunks = tag_str.split('/')
347 if len(chunks) == 2 and chunks[0] == 'autor':
348 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
350 except Tag.MultipleObjectsReturned as e:
351 # Ask the user to disambiguate
352 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
353 except Tag.UrlDeprecationWarning as e:
354 raise ResponseInstead(HttpResponsePermanentRedirect(
355 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
360 if len(tags) > settings.MAX_TAG_LIST:
362 except AttributeError:
368 def tagged_object_list(request, tags, list_type):
369 return TaggedObjectList.as_view()(request, tags=tags)
372 def book_fragments(request, slug, theme_slug):
373 book = get_object_or_404(Book, slug=slug)
374 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
375 fragments = Fragment.tagged.with_all([theme]).filter(
376 Q(book=book) | Q(book__ancestor=book))
378 template_name = 'catalogue/book_fragments.html'
385 'fragments': fragments,
390 def book_detail(request, slug):
392 book = Book.objects.get(slug=slug)
393 except Book.DoesNotExist:
394 return pdcounter_views.book_stub_detail(request, slug)
398 'catalogue/book_detail.html',
401 'accessible': book.is_accessible_to(request.user),
402 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
403 'club': Club.objects.first() if book.preview else None,
404 'donation_form': DonationStep1Form(),
408 def book_text(request, slug):
409 book = get_object_or_404(Book, slug=slug)
411 if not book.is_accessible_to(request.user):
412 return HttpResponseRedirect(book.get_absolute_url())
414 if not book.has_html_file():
416 with book.html_file.open('r') as f:
419 return render(request, 'catalogue/book_text.html', {
421 'extra_info': book.get_extra_info_json(),
422 'book_text': book_text,
423 'inserts': DynamicTextInsert.get_all(request),
425 'club': Club.objects.first(),
426 'donation_form': DonationStep1Form(),
435 def import_book(request):
436 """docstring for import_book"""
437 book_import_form = forms.BookImportForm(request.POST, request.FILES)
438 if book_import_form.is_valid():
440 book_import_form.save()
445 info = sys.exc_info()
446 exception = pprint.pformat(info[1])
447 tb = '\n'.join(traceback.format_tb(info[2]))
449 "Błąd: %(exception)s\n\n%(tb)s" % {
450 'exception': exception, 'tb': tb
452 content_type='text/plain'
454 return HttpResponse("Książka zaimportowana")
455 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
460 def book_info(request, book_id, lang='pl'):
461 book = get_object_or_404(Book, id=book_id)
462 # set language by hand
463 translation.activate(lang)
464 return render(request, 'catalogue/book_info.html', {'book': book})
467 def tag_info(request, tag_id):
468 tag = get_object_or_404(Tag, id=tag_id)
469 return HttpResponse(tag.description)
473 def embargo_link(request, key, format_, slug):
474 book = get_object_or_404(Book, slug=slug)
475 if format_ not in Book.formats:
477 if key != book.preview_key:
479 media_file = book.get_media(format_)
481 return HttpResponseRedirect(media_file.url)
482 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
485 def download_zip(request, file_format=None, media_format=None, slug=None):
487 url = Book.zip_format(file_format)
488 elif media_format and slug is not None:
489 book = get_object_or_404(Book, slug=slug)
490 url = book.zip_audiobooks(media_format)
492 raise Http404('No format specified for zip package')
493 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
496 class CustomPDFFormView(AjaxableFormView):
497 form_class = forms.CustomPDFForm
498 title = gettext_lazy('Stwórz własny PDF')
499 submit = gettext_lazy('Pobierz')
500 template = 'catalogue/custom_pdf_form.html'
503 def __call__(self, *args, **kwargs):
504 if settings.NO_CUSTOM_PDF:
505 raise Http404('Custom PDF is disabled')
506 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
508 def form_args(self, request, obj):
509 """Override to parse view args and give additional args to the form."""
512 def validate_object(self, obj, request):
514 if not book.is_accessible_to(request.user):
515 return HttpResponseRedirect(book.get_absolute_url())
516 return super(CustomPDFFormView, self).validate_object(obj, request)
518 def get_object(self, request, slug, *args, **kwargs):
519 book = get_object_or_404(Book, slug=slug)
522 def context_description(self, request, obj):
523 return obj.pretty_title()
526 def tag_catalogue(request, category):
527 if category == 'theme':
528 tags = Tag.objects.usage_for_model(
529 Fragment, counts=True).filter(category='theme')
531 tags = list(get_top_level_related_tags((), categories=(category,)))
533 described_tags = [tag for tag in tags if tag.description]
535 if len(described_tags) > 4:
536 best = random.sample(described_tags, 4)
538 best = described_tags
540 template_name = 'catalogue/tag_catalogue.html'
541 return render(request, template_name, {
544 'title': constants.CATEGORIES_NAME_PLURAL[category],
545 'whole_category': constants.WHOLE_CATEGORY[category],
549 def collections(request):
550 objects = Collection.objects.filter(listed=True)
553 best = random.sample(list(objects), 4)
557 template_name = 'catalogue/collections.html'
558 return render(request, template_name, {
564 def ridero_cover(request, slug):
565 from librarian.cover import make_cover
566 wldoc = Book.objects.get(slug=slug).wldocument()
567 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
568 response = HttpResponse(content_type="image/png")
573 def get_isbn(request, book_format, slug):
574 book = Book.objects.get(slug=slug)
575 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))