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'])
217 if len(self.ctx['tags']) > 4:
224 for tag in self.ctx['tags']:
225 if isinstance(tag, UserList):
226 self.ctx['user_lists'].append(tag)
227 elif tag.category == 'theme':
228 self.ctx['fragment_tags'].append(tag)
230 self.ctx['work_tags'].append(tag)
232 self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
234 self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
235 elif self.ctx['work_tags']:
236 self.ctx['main_tag'] = self.ctx['work_tags'][0]
238 self.ctx['main_tag'] = None
239 self.ctx['filtering_tags'] = [
240 t for t in self.ctx['tags']
241 if t is not self.ctx['main_tag']
243 if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None and self.ctx['main_tag'].category == 'author':
244 self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
245 self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
247 def get_queryset(self):
248 if self.ctx['work_tags']:
249 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
251 qs = Book.objects.filter(findable=True)
252 for ul in self.ctx['user_lists']:
253 qs = qs.filter(id__in=[i.id for i in ul.get_books()])
254 qs = qs.exclude(ancestor__in=qs)
256 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
257 if self.ctx['work_tags']:
259 Q(book__in=qs) | Q(book__ancestor__in=qs)
264 def get_suggested_tags(self, queryset):
265 if self.ctx['user_lists']:
268 tag_ids = [t.id for t in self.ctx['tags']]
271 current_books = self.get_queryset().values_list('book', flat=True).distinct()
272 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
274 related_tags.extend(list(
275 Tag.objects.usage_for_queryset(
277 ).exclude(category='set').exclude(pk__in=tag_ids)
279 ### FIXME: These won't be tags
280 if self.request.user.is_authenticated:
281 related_tags.extend(list(
282 Tag.objects.usage_for_queryset(
285 user=self.request.user
286 ).exclude(name='').exclude(pk__in=tag_ids)
289 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
290 if self.request.user.is_authenticated:
291 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
292 related_tags.extend(list(
293 Tag.objects.usage_for_queryset(
296 user=self.request.user
297 ).exclude(name='').exclude(pk__in=tag_ids)
300 fragments = Fragment.objects.filter(
301 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
304 Tag.objects.usage_for_queryset(
305 fragments, counts=True
306 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
307 .only('name', 'sort_key', 'category', 'slug'))
313 def object_list(request, objects, list_type='books'):
314 related_tag_lists = []
316 related_tag_lists.append(
317 Tag.objects.usage_for_queryset(
319 ).exclude(category='set'))
320 ### FIXME: these won't be tags
321 if request.user.is_authenticated:
322 related_tag_lists.append(
323 Tag.objects.usage_for_queryset(
330 fragments = Fragment.objects.filter(book__in=objects)
331 related_tag_lists.append(
332 Tag.objects.usage_for_queryset(
333 fragments, counts=True
334 ).filter(category='theme')
335 .only('name', 'sort_key', 'category', 'slug'))
336 if isinstance(objects, QuerySet):
337 objects = prefetch_relations(objects, 'author')
339 categories = split_tags(*related_tag_lists)
341 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
342 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
344 objects = list(objects)
347 'object_list': objects,
349 'list_type': list_type,
352 template = 'catalogue/author_detail.html'
355 request, template, result,
359 class ResponseInstead(Exception):
360 def __init__(self, response):
361 super(ResponseInstead, self).__init__()
362 self.response = response
365 def analyse_tags(request, tag_str):
367 tags = Tag.get_tag_list(tag_str)
368 except Tag.DoesNotExist:
369 # Perhaps the user is asking about an author in Public Domain
370 # counter (they are not represented in tags)
371 chunks = tag_str.split('/')
372 if len(chunks) == 2 and chunks[0] == 'autor':
373 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
375 except Tag.MultipleObjectsReturned as e:
376 # Ask the user to disambiguate
377 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
378 except Tag.UrlDeprecationWarning as e:
379 raise ResponseInstead(HttpResponsePermanentRedirect(
380 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
385 if len(tags) > settings.MAX_TAG_LIST:
387 except AttributeError:
393 def tagged_object_list(request, tags, list_type):
394 return TaggedObjectList.as_view()(request, tags=tags)
397 def book_fragments(request, slug, theme_slug):
398 book = get_object_or_404(Book, slug=slug)
399 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
400 fragments = Fragment.tagged.with_all([theme]).filter(
401 Q(book=book) | Q(book__ancestor=book))
403 template_name = 'catalogue/book_fragments.html'
410 'fragments': fragments,
415 def book_detail(request, slug):
417 book = Book.objects.get(slug=slug)
418 except Book.DoesNotExist:
419 return pdcounter_views.book_stub_detail(request, slug)
423 'catalogue/book_detail.html',
426 'accessible': book.is_accessible_to(request.user),
427 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
428 'club': Club.objects.first() if book.preview else None,
429 'donation_form': DonationStep1Form(),
433 def book_text(request, slug):
434 book = get_object_or_404(Book, slug=slug)
436 if not book.is_accessible_to(request.user):
437 return HttpResponseRedirect(book.get_absolute_url())
439 if not book.has_html_file():
441 with book.html_file.open('r') as f:
444 return render(request, 'catalogue/book_text.html', {
446 'extra_info': book.get_extra_info_json(),
447 'book_text': book_text,
448 'inserts': DynamicTextInsert.get_all(request),
450 'club': Club.objects.first(),
451 'donation_form': DonationStep1Form(),
460 def import_book(request):
461 """docstring for import_book"""
462 book_import_form = forms.BookImportForm(request.POST, request.FILES)
463 if book_import_form.is_valid():
465 book_import_form.save()
470 info = sys.exc_info()
471 exception = pprint.pformat(info[1])
472 tb = '\n'.join(traceback.format_tb(info[2]))
474 "Błąd: %(exception)s\n\n%(tb)s" % {
475 'exception': exception, 'tb': tb
477 content_type='text/plain'
479 return HttpResponse("Książka zaimportowana")
480 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
485 def book_info(request, book_id, lang='pl'):
486 book = get_object_or_404(Book, id=book_id)
487 # set language by hand
488 translation.activate(lang)
489 return render(request, 'catalogue/book_info.html', {'book': book})
492 def tag_info(request, tag_id):
493 tag = get_object_or_404(Tag, id=tag_id)
494 return HttpResponse(tag.description)
498 def embargo_link(request, key, format_, slug):
499 book = get_object_or_404(Book, slug=slug)
500 if format_ not in Book.formats:
502 if key != book.preview_key:
504 media_file = book.get_media(format_)
506 return HttpResponseRedirect(media_file.url)
507 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
510 def download_zip(request, file_format=None, media_format=None, slug=None):
512 url = Book.zip_format(file_format)
513 elif media_format and slug is not None:
514 book = get_object_or_404(Book, slug=slug)
515 url = book.zip_audiobooks(media_format)
517 raise Http404('No format specified for zip package')
518 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
521 class CustomPDFFormView(AjaxableFormView):
522 form_class = forms.CustomPDFForm
523 title = gettext_lazy('Stwórz własny PDF')
524 submit = gettext_lazy('Pobierz')
525 template = 'catalogue/custom_pdf_form.html'
528 def __call__(self, *args, **kwargs):
529 if settings.NO_CUSTOM_PDF:
530 raise Http404('Custom PDF is disabled')
531 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
533 def form_args(self, request, obj):
534 """Override to parse view args and give additional args to the form."""
537 def validate_object(self, obj, request):
539 if not book.is_accessible_to(request.user):
540 return HttpResponseRedirect(book.get_absolute_url())
541 return super(CustomPDFFormView, self).validate_object(obj, request)
543 def get_object(self, request, slug, *args, **kwargs):
544 book = get_object_or_404(Book, slug=slug)
547 def context_description(self, request, obj):
548 return obj.pretty_title()
551 def tag_catalogue(request, category):
552 if category == 'theme':
553 tags = Tag.objects.usage_for_model(
554 Fragment, counts=True).filter(category='theme')
556 tags = list(get_top_level_related_tags((), categories=(category,)))
558 described_tags = [tag for tag in tags if tag.description]
560 if len(described_tags) > 4:
561 best = random.sample(described_tags, 4)
563 best = described_tags
565 template_name = 'catalogue/tag_catalogue.html'
566 return render(request, template_name, {
569 'title': constants.CATEGORIES_NAME_PLURAL[category],
570 'whole_category': constants.WHOLE_CATEGORY[category],
574 def collections(request):
575 objects = Collection.objects.filter(listed=True)
578 best = random.sample(list(objects), 4)
582 template_name = 'catalogue/collections.html'
583 return render(request, template_name, {
589 def ridero_cover(request, slug):
590 from librarian.cover import make_cover
591 wldoc = Book.objects.get(slug=slug).wldocument()
592 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
593 response = HttpResponse(content_type="image/png")
598 def get_isbn(request, book_format, slug):
599 book = Book.objects.get(slug=slug)
600 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))