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()
231 def get_queryset(self):
232 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
233 qs = qs.exclude(ancestor__in=qs)
235 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
236 if self.ctx['work_tags']:
238 Q(book__in=qs) | Q(book__ancestor__in=qs)
243 def get_suggested_tags(self, queryset):
244 tag_ids = [t.id for t in self.ctx['tags']]
247 current_books = self.get_queryset().values_list('book', flat=True).distinct()
248 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
250 related_tags.extend(list(
251 Tag.objects.usage_for_queryset(
253 ).exclude(category='set').exclude(pk__in=tag_ids)
255 if self.request.user.is_authenticated:
256 related_tags.extend(list(
257 Tag.objects.usage_for_queryset(
260 user=self.request.user
261 ).exclude(name='').exclude(pk__in=tag_ids)
264 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
265 if self.request.user.is_authenticated:
266 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
267 related_tags.extend(list(
268 Tag.objects.usage_for_queryset(
271 user=self.request.user
272 ).exclude(name='').exclude(pk__in=tag_ids)
275 fragments = Fragment.objects.filter(
276 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
279 Tag.objects.usage_for_queryset(
280 fragments, counts=True
281 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
282 .only('name', 'sort_key', 'category', 'slug'))
288 def object_list(request, objects, list_type='books'):
289 related_tag_lists = []
291 related_tag_lists.append(
292 Tag.objects.usage_for_queryset(
294 ).exclude(category='set'))
295 if request.user.is_authenticated:
296 related_tag_lists.append(
297 Tag.objects.usage_for_queryset(
304 fragments = Fragment.objects.filter(book__in=objects)
305 related_tag_lists.append(
306 Tag.objects.usage_for_queryset(
307 fragments, counts=True
308 ).filter(category='theme')
309 .only('name', 'sort_key', 'category', 'slug'))
310 if isinstance(objects, QuerySet):
311 objects = prefetch_relations(objects, 'author')
313 categories = split_tags(*related_tag_lists)
315 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
316 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
318 objects = list(objects)
321 'object_list': objects,
323 'list_type': list_type,
326 template = 'catalogue/author_detail.html'
329 request, template, result,
333 class ResponseInstead(Exception):
334 def __init__(self, response):
335 super(ResponseInstead, self).__init__()
336 self.response = response
339 def analyse_tags(request, tag_str):
341 tags = Tag.get_tag_list(tag_str)
342 except Tag.DoesNotExist:
343 # Perhaps the user is asking about an author in Public Domain
344 # counter (they are not represented in tags)
345 chunks = tag_str.split('/')
346 if len(chunks) == 2 and chunks[0] == 'autor':
347 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
349 except Tag.MultipleObjectsReturned as e:
350 # Ask the user to disambiguate
351 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
352 except Tag.UrlDeprecationWarning as e:
353 raise ResponseInstead(HttpResponsePermanentRedirect(
354 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
359 if len(tags) > settings.MAX_TAG_LIST:
361 except AttributeError:
367 def tagged_object_list(request, tags, list_type):
368 return TaggedObjectList.as_view()(request, tags=tags)
371 def book_fragments(request, slug, theme_slug):
372 book = get_object_or_404(Book, slug=slug)
373 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
374 fragments = Fragment.tagged.with_all([theme]).filter(
375 Q(book=book) | Q(book__ancestor=book))
377 template_name = 'catalogue/book_fragments.html'
384 'fragments': fragments,
389 def book_detail(request, slug):
391 book = Book.objects.get(slug=slug)
392 except Book.DoesNotExist:
393 return pdcounter_views.book_stub_detail(request, slug)
397 'catalogue/book_detail.html',
400 'accessible': book.is_accessible_to(request.user),
401 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
402 'club': Club.objects.first() if book.preview else None,
403 'donation_form': DonationStep1Form(),
407 def book_text(request, slug):
408 book = get_object_or_404(Book, slug=slug)
410 if not book.is_accessible_to(request.user):
411 return HttpResponseRedirect(book.get_absolute_url())
413 if not book.has_html_file():
415 with book.html_file.open('r') as f:
418 return render(request, 'catalogue/book_text.html', {
420 'extra_info': book.get_extra_info_json(),
421 'book_text': book_text,
422 'inserts': DynamicTextInsert.get_all(request),
424 'club': Club.objects.first(),
425 'donation_form': DonationStep1Form(),
434 def import_book(request):
435 """docstring for import_book"""
436 book_import_form = forms.BookImportForm(request.POST, request.FILES)
437 if book_import_form.is_valid():
439 book_import_form.save()
444 info = sys.exc_info()
445 exception = pprint.pformat(info[1])
446 tb = '\n'.join(traceback.format_tb(info[2]))
448 "Błąd: %(exception)s\n\n%(tb)s" % {
449 'exception': exception, 'tb': tb
451 content_type='text/plain'
453 return HttpResponse("Książka zaimportowana")
454 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
459 def book_info(request, book_id, lang='pl'):
460 book = get_object_or_404(Book, id=book_id)
461 # set language by hand
462 translation.activate(lang)
463 return render(request, 'catalogue/book_info.html', {'book': book})
466 def tag_info(request, tag_id):
467 tag = get_object_or_404(Tag, id=tag_id)
468 return HttpResponse(tag.description)
472 def embargo_link(request, key, format_, slug):
473 book = get_object_or_404(Book, slug=slug)
474 if format_ not in Book.formats:
476 if key != book.preview_key:
478 media_file = book.get_media(format_)
480 return HttpResponseRedirect(media_file.url)
481 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
484 def download_zip(request, file_format=None, media_format=None, slug=None):
486 url = Book.zip_format(file_format)
487 elif media_format and slug is not None:
488 book = get_object_or_404(Book, slug=slug)
489 url = book.zip_audiobooks(media_format)
491 raise Http404('No format specified for zip package')
492 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
495 class CustomPDFFormView(AjaxableFormView):
496 form_class = forms.CustomPDFForm
497 title = gettext_lazy('Stwórz własny PDF')
498 submit = gettext_lazy('Pobierz')
499 template = 'catalogue/custom_pdf_form.html'
502 def __call__(self, *args, **kwargs):
503 if settings.NO_CUSTOM_PDF:
504 raise Http404('Custom PDF is disabled')
505 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
507 def form_args(self, request, obj):
508 """Override to parse view args and give additional args to the form."""
511 def validate_object(self, obj, request):
513 if not book.is_accessible_to(request.user):
514 return HttpResponseRedirect(book.get_absolute_url())
515 return super(CustomPDFFormView, self).validate_object(obj, request)
517 def get_object(self, request, slug, *args, **kwargs):
518 book = get_object_or_404(Book, slug=slug)
521 def context_description(self, request, obj):
522 return obj.pretty_title()
525 def tag_catalogue(request, category):
526 if category == 'theme':
527 tags = Tag.objects.usage_for_model(
528 Fragment, counts=True).filter(category='theme')
530 tags = list(get_top_level_related_tags((), categories=(category,)))
532 described_tags = [tag for tag in tags if tag.description]
534 if len(described_tags) > 4:
535 best = random.sample(described_tags, 4)
537 best = described_tags
539 template_name = 'catalogue/tag_catalogue.html'
540 return render(request, template_name, {
543 'title': constants.CATEGORIES_NAME_PLURAL[category],
544 'whole_category': constants.WHOLE_CATEGORY[category],
548 def collections(request):
549 objects = Collection.objects.filter(listed=True)
552 best = random.sample(list(objects), 4)
556 template_name = 'catalogue/collections.html'
557 return render(request, template_name, {
563 def ridero_cover(request, slug):
564 from librarian.cover import make_cover
565 wldoc = Book.objects.get(slug=slug).wldocument()
566 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
567 response = HttpResponse(content_type="image/png")
572 def get_isbn(request, book_format, slug):
573 book = Book.objects.get(slug=slug)
574 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))