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()
244 def get_queryset(self):
245 if self.ctx['work_tags']:
246 qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
248 qs = Book.objects.filter(findable=True)
249 for ul in self.ctx['user_lists']:
250 qs = qs.filter(id__in=[i.id for i in ul.get_books()])
251 qs = qs.exclude(ancestor__in=qs)
253 fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
254 if self.ctx['work_tags']:
256 Q(book__in=qs) | Q(book__ancestor__in=qs)
261 def get_suggested_tags(self, queryset):
262 if self.ctx['user_lists']:
265 tag_ids = [t.id for t in self.ctx['tags']]
268 current_books = self.get_queryset().values_list('book', flat=True).distinct()
269 containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
271 related_tags.extend(list(
272 Tag.objects.usage_for_queryset(
274 ).exclude(category='set').exclude(pk__in=tag_ids)
276 ### FIXME: These won't be tags
277 if self.request.user.is_authenticated:
278 related_tags.extend(list(
279 Tag.objects.usage_for_queryset(
282 user=self.request.user
283 ).exclude(name='').exclude(pk__in=tag_ids)
286 related_tags = list(get_top_level_related_tags(self.ctx['tags']))
287 if self.request.user.is_authenticated:
288 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
289 related_tags.extend(list(
290 Tag.objects.usage_for_queryset(
293 user=self.request.user
294 ).exclude(name='').exclude(pk__in=tag_ids)
297 fragments = Fragment.objects.filter(
298 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
301 Tag.objects.usage_for_queryset(
302 fragments, counts=True
303 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
304 .only('name', 'sort_key', 'category', 'slug'))
310 def object_list(request, objects, list_type='books'):
311 related_tag_lists = []
313 related_tag_lists.append(
314 Tag.objects.usage_for_queryset(
316 ).exclude(category='set'))
317 ### FIXME: these won't be tags
318 if request.user.is_authenticated:
319 related_tag_lists.append(
320 Tag.objects.usage_for_queryset(
327 fragments = Fragment.objects.filter(book__in=objects)
328 related_tag_lists.append(
329 Tag.objects.usage_for_queryset(
330 fragments, counts=True
331 ).filter(category='theme')
332 .only('name', 'sort_key', 'category', 'slug'))
333 if isinstance(objects, QuerySet):
334 objects = prefetch_relations(objects, 'author')
336 categories = split_tags(*related_tag_lists)
338 for c in ['set', 'author', 'epoch', 'kind', 'genre']:
339 suggest.extend(sorted(categories[c], key=lambda t: -t.count))
341 objects = list(objects)
344 'object_list': objects,
346 'list_type': list_type,
349 template = 'catalogue/author_detail.html'
352 request, template, result,
356 class ResponseInstead(Exception):
357 def __init__(self, response):
358 super(ResponseInstead, self).__init__()
359 self.response = response
362 def analyse_tags(request, tag_str):
364 tags = Tag.get_tag_list(tag_str)
365 except Tag.DoesNotExist:
366 # Perhaps the user is asking about an author in Public Domain
367 # counter (they are not represented in tags)
368 chunks = tag_str.split('/')
369 if len(chunks) == 2 and chunks[0] == 'autor':
370 raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
372 except Tag.MultipleObjectsReturned as e:
373 # Ask the user to disambiguate
374 raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
375 except Tag.UrlDeprecationWarning as e:
376 raise ResponseInstead(HttpResponsePermanentRedirect(
377 reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
382 if len(tags) > settings.MAX_TAG_LIST:
384 except AttributeError:
390 def tagged_object_list(request, tags, list_type):
391 return TaggedObjectList.as_view()(request, tags=tags)
394 def book_fragments(request, slug, theme_slug):
395 book = get_object_or_404(Book, slug=slug)
396 theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
397 fragments = Fragment.tagged.with_all([theme]).filter(
398 Q(book=book) | Q(book__ancestor=book))
400 template_name = 'catalogue/book_fragments.html'
407 'fragments': fragments,
412 def book_detail(request, slug):
414 book = Book.objects.get(slug=slug)
415 except Book.DoesNotExist:
416 return pdcounter_views.book_stub_detail(request, slug)
420 'catalogue/book_detail.html',
423 'accessible': book.is_accessible_to(request.user),
424 'book_children': book.children.all().order_by('parent_number', 'sort_key'),
425 'club': Club.objects.first() if book.preview else None,
426 'donation_form': DonationStep1Form(),
430 def book_text(request, slug):
431 book = get_object_or_404(Book, slug=slug)
433 if not book.is_accessible_to(request.user):
434 return HttpResponseRedirect(book.get_absolute_url())
436 if not book.has_html_file():
438 with book.html_file.open('r') as f:
441 return render(request, 'catalogue/book_text.html', {
443 'extra_info': book.get_extra_info_json(),
444 'book_text': book_text,
445 'inserts': DynamicTextInsert.get_all(request),
447 'club': Club.objects.first(),
448 'donation_form': DonationStep1Form(),
457 def import_book(request):
458 """docstring for import_book"""
459 book_import_form = forms.BookImportForm(request.POST, request.FILES)
460 if book_import_form.is_valid():
462 book_import_form.save()
467 info = sys.exc_info()
468 exception = pprint.pformat(info[1])
469 tb = '\n'.join(traceback.format_tb(info[2]))
471 "Błąd: %(exception)s\n\n%(tb)s" % {
472 'exception': exception, 'tb': tb
474 content_type='text/plain'
476 return HttpResponse("Książka zaimportowana")
477 return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
482 def book_info(request, book_id, lang='pl'):
483 book = get_object_or_404(Book, id=book_id)
484 # set language by hand
485 translation.activate(lang)
486 return render(request, 'catalogue/book_info.html', {'book': book})
489 def tag_info(request, tag_id):
490 tag = get_object_or_404(Tag, id=tag_id)
491 return HttpResponse(tag.description)
495 def embargo_link(request, key, format_, slug):
496 book = get_object_or_404(Book, slug=slug)
497 if format_ not in Book.formats:
499 if key != book.preview_key:
501 media_file = book.get_media(format_)
503 return HttpResponseRedirect(media_file.url)
504 return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
507 def download_zip(request, file_format=None, media_format=None, slug=None):
509 url = Book.zip_format(file_format)
510 elif media_format and slug is not None:
511 book = get_object_or_404(Book, slug=slug)
512 url = book.zip_audiobooks(media_format)
514 raise Http404('No format specified for zip package')
515 return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
518 class CustomPDFFormView(AjaxableFormView):
519 form_class = forms.CustomPDFForm
520 title = gettext_lazy('Stwórz własny PDF')
521 submit = gettext_lazy('Pobierz')
522 template = 'catalogue/custom_pdf_form.html'
525 def __call__(self, *args, **kwargs):
526 if settings.NO_CUSTOM_PDF:
527 raise Http404('Custom PDF is disabled')
528 return super(CustomPDFFormView, self).__call__(*args, **kwargs)
530 def form_args(self, request, obj):
531 """Override to parse view args and give additional args to the form."""
534 def validate_object(self, obj, request):
536 if not book.is_accessible_to(request.user):
537 return HttpResponseRedirect(book.get_absolute_url())
538 return super(CustomPDFFormView, self).validate_object(obj, request)
540 def get_object(self, request, slug, *args, **kwargs):
541 book = get_object_or_404(Book, slug=slug)
544 def context_description(self, request, obj):
545 return obj.pretty_title()
548 def tag_catalogue(request, category):
549 if category == 'theme':
550 tags = Tag.objects.usage_for_model(
551 Fragment, counts=True).filter(category='theme')
553 tags = list(get_top_level_related_tags((), categories=(category,)))
555 described_tags = [tag for tag in tags if tag.description]
557 if len(described_tags) > 4:
558 best = random.sample(described_tags, 4)
560 best = described_tags
562 template_name = 'catalogue/tag_catalogue.html'
563 return render(request, template_name, {
566 'title': constants.CATEGORIES_NAME_PLURAL[category],
567 'whole_category': constants.WHOLE_CATEGORY[category],
571 def collections(request):
572 objects = Collection.objects.filter(listed=True)
575 best = random.sample(list(objects), 4)
579 template_name = 'catalogue/collections.html'
580 return render(request, template_name, {
586 def ridero_cover(request, slug):
587 from librarian.cover import make_cover
588 wldoc = Book.objects.get(slug=slug).wldocument()
589 cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
590 response = HttpResponse(content_type="image/png")
595 def get_isbn(request, book_format, slug):
596 book = Book.objects.get(slug=slug)
597 return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))