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 social.models import UserList
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 'collections': Collection.objects.filter(listed=True),
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, {
58 def differentiate_tags(request, tags, ambiguous_slugs):
59 beginning = '/'.join(tag.url_chunk for tag in tags)
60 unparsed = '/'.join(ambiguous_slugs[1:])
62 for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
64 'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
69 'catalogue/differentiate_tags.html',
70 {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
74 from django.db.models import FilteredRelation, Q
75 from django.views.decorators.cache import cache_control
76 from django.views.decorators.vary import vary_on_headers
77 from django.utils.decorators import method_decorator
81 vary_on_headers('X-Requested-With'),
82 cache_control(max_age=1),
84 class ObjectListView(TemplateView):
87 item_template_name = ''
89 default_ordering = None
92 self.is_themed = False
96 def dispatch(self, *args, **kwargs):
99 except ResponseInstead as e:
101 return super().dispatch(*args, **kwargs)
103 def get_orderings(self):
104 order = self.get_order()
109 "active": k == order,
110 "default": v[0] is None,
112 for k, v in self.orderings.items()
116 order = self.request.GET.get('order')
117 if order not in self.orderings:
118 order = self.default_ordering
122 order_tag = self.get_order()
124 order = self.orderings[order_tag]
127 qs = qs.order_by(order_by)
130 def search(self, qs):
133 def get_template_names(self):
134 if is_ajax(self.request) or self.request.GET.get('dyn'):
136 return self.dynamic_themed_template_name
138 return self.dynamic_template_name
141 return self.themed_template_name
143 return self.template_name
145 def get_context_data(self, **kwargs):
146 ctx = super().get_context_data()
148 qs = self.get_queryset()
152 ctx['object_list'] = qs
153 ctx['suggested_tags'] = self.get_suggested_tags(qs)
154 ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
158 class BookList(ObjectListView):
159 title = gettext_lazy('Literatura')
161 template_name = 'catalogue/book_list.html'
162 dynamic_template_name = 'catalogue/dynamic_book_list.html'
163 themed_template_name = 'catalogue/themed_book_list.html'
164 dynamic_themed_template_name = 'catalogue/dynamic_themed_book_list.html'
167 'pop': ('-popularity__count', gettext_lazy('najpopularniejsze')),
168 'alpha': (None, gettext_lazy('alfabetycznie')),
170 default_ordering = 'alpha'
172 def get_queryset(self):
173 return Book.objects.filter(parent=None, findable=True)
175 def search(self, qs):
176 term = self.request.GET.get('search')
178 meta_rels = TagRelation.objects.filter(tag__category='author')
179 # TODO: search tags in currently displaying language
181 rels = meta_rels.filter(tag__name_pl__icontains=term)
183 Q(book__title__icontains=term) |
184 Q(tag_relations__in=rels) |
185 Q(text__icontains=term)
189 meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
191 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
195 class LiteratureView(BookList):
196 def get_suggested_tags(self, queryset):
197 tags = list(get_top_level_related_tags([]))
198 tags.sort(key=lambda t: -t.count)
199 if self.request.user.is_authenticated:
200 tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
204 class AudiobooksView(LiteratureView):
205 title = gettext_lazy('Audiobooki')
206 list_type = 'audiobooks'
208 def get_queryset(self):
209 return Book.objects.filter(findable=True, media__type='mp3').distinct()
212 class TaggedObjectList(BookList):
216 self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
222 for tag in self.ctx['tags']:
223 if isinstance(tag, UserList):
224 self.ctx['user_lists'].append(tag)
225 elif tag.category == 'theme':
226 self.ctx['fragment_tags'].append(tag)
228 self.ctx['work_tags'].append(tag)
230 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
232 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
233 elif self.ctx['work_tags']:
234 self.ctx['main_tag'] = self.ctx['work_tags'][0]
236 self.ctx['main_tag'] = None
237 self.ctx['filtering_tags'] = [
238 t for t in self.ctx['tags']
239 if t is not self.ctx['main_tag']
241 if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None and self.ctx['main_tag'].category == 'author':
242 self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
243 self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
245 def get_queryset(self):
246 if self.ctx['work_tags']:
247 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
249 qs = Book.objects.filter(findable=True)
250 for ul in self.ctx['user_lists']:
251 qs = qs.filter(id__in=[i.id for i in ul.get_books()])
252 qs = qs.exclude(ancestor__in=qs)
254 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
255 if self.ctx['work_tags']:
257 Q(book__in=qs) | Q(book__ancestor__in=qs)
262 def get_suggested_tags(self, queryset):
263 if self.ctx['user_lists']:
266 tag_ids = [t.id for t in self.ctx['tags']]
269 current_books = self.get_queryset().values_list('book', flat=True).distinct()
270 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
272 related_tags.extend(list(
273 Tag.objects.usage_for_queryset(
275 ).exclude(category='set').exclude(pk__in=tag_ids)
277 ### FIXME: These won't be tags
278 if self.request.user.is_authenticated:
279 related_tags.extend(list(
280 Tag.objects.usage_for_queryset(
283 user=self.request.user
284 ).exclude(name='').exclude(pk__in=tag_ids)
287 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
288 if self.request.user.is_authenticated:
289 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
290 related_tags.extend(list(
291 Tag.objects.usage_for_queryset(
294 user=self.request.user
295 ).exclude(name='').exclude(pk__in=tag_ids)
298 fragments = Fragment.objects.filter(
299 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
302 Tag.objects.usage_for_queryset(
303 fragments, counts=True
304 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
305 .only('name', 'sort_key', 'category', 'slug'))
311 def object_list(request, objects, list_type='books'):
312 related_tag_lists = []
314 related_tag_lists.append(
315 Tag.objects.usage_for_queryset(
317 ).exclude(category='set'))
318 ### FIXME: these won't be tags
319 if request.user.is_authenticated:
320 related_tag_lists.append(
321 Tag.objects.usage_for_queryset(
328 fragments = Fragment.objects.filter(book__in=objects)
329 related_tag_lists.append(
330 Tag.objects.usage_for_queryset(
331 fragments, counts=True
332 ).filter(category='theme')
333 .only('name', 'sort_key', 'category', 'slug'))
334 if isinstance(objects, QuerySet):
335 objects = prefetch_relations(objects, 'author')
337 categories = split_tags(*related_tag_lists)
339 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
340 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
342 objects = list(objects)
345 'object_list': objects,
347 'list_type': list_type,
350 template = 'catalogue/author_detail.html'
353 request, template, result,
357 class ResponseInstead(Exception):
358 def __init__(self, response):
359 super(ResponseInstead, self).__init__()
360 self.response = response
363 def analyse_tags(request, tag_str):
365 tags = Tag.get_tag_list(tag_str)
366 except Tag.DoesNotExist:
367 # Perhaps the user is asking about an author in Public Domain
368 # counter (they are not represented in tags)
369 chunks = tag_str.split('/')
370 if len(chunks) == 2 and chunks[0] == 'autor':
371 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
373 except Tag.MultipleObjectsReturned as e:
374 # Ask the user to disambiguate
375 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
376 except Tag.UrlDeprecationWarning as e:
377 raise ResponseInstead(HttpResponsePermanentRedirect(
378 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
383 if len(tags) > settings.MAX_TAG_LIST:
385 except AttributeError:
391 def tagged_object_list(request, tags, list_type):
392 return TaggedObjectList.as_view()(request, tags=tags)
395 def book_fragments(request, slug, theme_slug):
396 book = get_object_or_404(Book, slug=slug)
397 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
398 fragments = Fragment.tagged.with_all([theme]).filter(
399 Q(book=book) | Q(book__ancestor=book))
401 template_name = 'catalogue/book_fragments.html'
408 'fragments': fragments,
413 def book_detail(request, slug):
415 book = Book.objects.get(slug=slug)
416 except Book.DoesNotExist:
417 return pdcounter_views.book_stub_detail(request, slug)
421 'catalogue/book_detail.html',
424 'accessible': book.is_accessible_to(request.user),
425 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
426 'club': Club.objects.first() if book.preview else None,
427 'donation_form': DonationStep1Form(),
431 def book_text(request, slug):
432 book = get_object_or_404(Book, slug=slug)
434 if not book.is_accessible_to(request.user):
435 return HttpResponseRedirect(book.get_absolute_url())
437 if not book.has_html_file():
439 with book.html_file.open('r') as f:
442 return render(request, 'catalogue/book_text.html', {
444 'extra_info': book.get_extra_info_json(),
445 'book_text': book_text,
446 'inserts': DynamicTextInsert.get_all(request),
448 'club': Club.objects.first(),
449 'donation_form': DonationStep1Form(),
458 def import_book(request):
459 """docstring for import_book"""
460 book_import_form = forms.BookImportForm(request.POST, request.FILES)
461 if book_import_form.is_valid():
463 book_import_form.save()
468 info = sys.exc_info()
469 exception = pprint.pformat(info[1])
470 tb = '\n'.join(traceback.format_tb(info[2]))
472 "Błąd: %(exception)s\n\n%(tb)s" % {
473 'exception': exception, 'tb': tb
475 content_type='text/plain'
477 return HttpResponse("Książka zaimportowana")
478 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
483 def book_info(request, book_id, lang='pl'):
484 book = get_object_or_404(Book, id=book_id)
485 # set language by hand
486 translation.activate(lang)
487 return render(request, 'catalogue/book_info.html', {'book': book})
490 def tag_info(request, tag_id):
491 tag = get_object_or_404(Tag, id=tag_id)
492 return HttpResponse(tag.description)
496 def embargo_link(request, key, format_, slug):
497 book = get_object_or_404(Book, slug=slug)
498 if format_ not in Book.formats:
500 if key != book.preview_key:
502 media_file = book.get_media(format_)
504 return HttpResponseRedirect(media_file.url)
505 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
508 def download_zip(request, file_format=None, media_format=None, slug=None):
510 url = Book.zip_format(file_format)
511 elif media_format and slug is not None:
512 book = get_object_or_404(Book, slug=slug)
513 url = book.zip_audiobooks(media_format)
515 raise Http404('No format specified for zip package')
516 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
519 class CustomPDFFormView(AjaxableFormView):
520 form_class = forms.CustomPDFForm
521 title = gettext_lazy('Stwórz własny PDF')
522 submit = gettext_lazy('Pobierz')
523 template = 'catalogue/custom_pdf_form.html'
526 def __call__(self, *args, **kwargs):
527 if settings.NO_CUSTOM_PDF:
528 raise Http404('Custom PDF is disabled')
529 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
531 def form_args(self, request, obj):
532 """Override to parse view args and give additional args to the form."""
535 def validate_object(self, obj, request):
537 if not book.is_accessible_to(request.user):
538 return HttpResponseRedirect(book.get_absolute_url())
539 return super(CustomPDFFormView, self).validate_object(obj, request)
541 def get_object(self, request, slug, *args, **kwargs):
542 book = get_object_or_404(Book, slug=slug)
545 def context_description(self, request, obj):
546 return obj.pretty_title()
549 def tag_catalogue(request, category):
550 if category == 'theme':
551 tags = Tag.objects.usage_for_model(
552 Fragment, counts=True).filter(category='theme')
554 tags = list(get_top_level_related_tags((), categories=(category,)))
556 described_tags = [tag for tag in tags if tag.description]
558 if len(described_tags) > 4:
559 best = random.sample(described_tags, 4)
561 best = described_tags
563 template_name = 'catalogue/tag_catalogue.html'
564 return render(request, template_name, {
567 'title': constants.CATEGORIES_NAME_PLURAL[category],
568 'whole_category': constants.WHOLE_CATEGORY[category],
572 def collections(request):
573 objects = Collection.objects.filter(listed=True)
576 best = random.sample(list(objects), 4)
580 template_name = 'catalogue/collections.html'
581 return render(request, template_name, {
587 def ridero_cover(request, slug):
588 from librarian.cover import make_cover
589 wldoc = Book.objects.get(slug=slug).wldocument()
590 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
591 response = HttpResponse(content_type="image/png")
596 def get_isbn(request, book_format, slug):
597 book = Book.objects.get(slug=slug)
598 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))