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),
 
  42         'active_menu_item': 'all_works',
 
  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, {
 
  55         'active_menu_item': 'collections',
 
  59 def differentiate_tags(request, tags, ambiguous_slugs):
 
  60     beginning = '/'.join(tag.url_chunk for tag in tags)
 
  61     unparsed = '/'.join(ambiguous_slugs[1:])
 
  63     for tag in Tag.objects.filter(slug=ambiguous_slugs[0]):
 
  65             'url_args': '/'.join((beginning, tag.url_chunk, unparsed)).strip('/'),
 
  70         'catalogue/differentiate_tags.html',
 
  71         {'tags': tags, 'options': options, 'unparsed': ambiguous_slugs[1:]}
 
  75 from django.db.models import FilteredRelation, Q
 
  76 from django.views.decorators.cache import cache_control
 
  77 from django.views.decorators.vary import vary_on_headers
 
  78 from django.utils.decorators import method_decorator
 
  82     vary_on_headers('X-Requested-With'),
 
  83     cache_control(max_age=1),
 
  85 class ObjectListView(TemplateView):
 
  88     item_template_name = ''
 
  90     default_ordering = None
 
  93         self.is_themed = False
 
  97     def dispatch(self, *args, **kwargs):
 
 100         except ResponseInstead as e:
 
 102         return super().dispatch(*args, **kwargs)
 
 104     def get_orderings(self):
 
 105         order = self.get_order()
 
 110                 "active": k == order,
 
 111                 "default": v[0] is None,
 
 113             for k, v in self.orderings.items()
 
 117         order = self.request.GET.get('order')
 
 118         if order not in self.orderings:
 
 119             order = self.default_ordering
 
 123         order_tag = self.get_order()
 
 125             order = self.orderings[order_tag]
 
 128                 qs = qs.order_by(order_by)
 
 131     def search(self, qs):
 
 134     def get_template_names(self):
 
 135         if is_ajax(self.request) or self.request.GET.get('dyn'):
 
 137                 return self.dynamic_themed_template_name
 
 139                 return self.dynamic_template_name
 
 142                 return self.themed_template_name
 
 144                 return self.template_name
 
 146     def get_context_data(self, **kwargs):
 
 147         ctx = super().get_context_data()
 
 149         qs = self.get_queryset()
 
 153         ctx['object_list'] = qs
 
 154         ctx['suggested_tags'] = self.get_suggested_tags(qs)
 
 155         ctx['suggested_tags_by_category'] = split_tags(ctx['suggested_tags'])
 
 159 class BookList(ObjectListView):
 
 160     title = gettext_lazy('Literatura')
 
 162     template_name = 'catalogue/book_list.html'
 
 163     dynamic_template_name = 'catalogue/dynamic_book_list.html'
 
 164     themed_template_name = 'catalogue/themed_book_list.html'
 
 165     dynamic_themed_template_name = 'catalogue/dynamic_themed_book_list.html'
 
 168         'pop': ('-popularity__count', gettext_lazy('najpopularniejsze')),
 
 169         'alpha': (None, gettext_lazy('alfabetycznie')),
 
 171     default_ordering = 'alpha'
 
 173     def get_queryset(self):
 
 174         return Book.objects.filter(parent=None, findable=True)
 
 176     def search(self, qs):
 
 177         term = self.request.GET.get('search')
 
 179             meta_rels = TagRelation.objects.filter(tag__category='author')
 
 180             # TODO: search tags in currently displaying language
 
 182                 rels = meta_rels.filter(tag__name_pl__icontains=term)
 
 184                     Q(book__title__icontains=term) |
 
 185                     Q(tag_relations__in=rels) |
 
 186                     Q(text__icontains=term)
 
 190                     meta=FilteredRelation('tag_relations', condition=Q(tag_relations__in=meta_rels))
 
 192                 qs = qs.filter(Q(title__icontains=term) | Q(meta__tag__name_pl__icontains=term)).distinct()
 
 196 class LiteratureView(BookList):
 
 197     def get_suggested_tags(self, queryset):
 
 198         tags = list(get_top_level_related_tags([]))
 
 199         tags.sort(key=lambda t: -t.count)
 
 200         if self.request.user.is_authenticated:
 
 201             tags.extend(list(Tag.objects.filter(user=self.request.user).exclude(name='')))
 
 205 class AudiobooksView(LiteratureView):
 
 206     title = gettext_lazy('Audiobooki')
 
 207     list_type = 'audiobooks'
 
 209     def get_queryset(self):
 
 210         return Book.objects.filter(findable=True, media__type='mp3').distinct()
 
 213 class TaggedObjectList(BookList):
 
 216         self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
 
 217         self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
 
 218         self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
 
 219         self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
 
 221             self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
 
 222         elif self.ctx['tags']:
 
 223             self.ctx['main_tag'] = self.ctx['tags'][0]
 
 225             self.ctx['main_tag'] = None
 
 226         self.ctx['filtering_tags'] = [
 
 227             t for t in self.ctx['tags']
 
 228             if t is not self.ctx['main_tag']
 
 230         if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
 
 231             self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
 
 233     def get_queryset(self):
 
 234         qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
 
 235         qs = qs.exclude(ancestor__in=qs)
 
 237             fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
 
 238             if self.ctx['work_tags']:
 
 240                     Q(book__in=qs) | Q(book__ancestor__in=qs)
 
 245     def get_suggested_tags(self, queryset):
 
 246         tag_ids = [t.id for t in self.ctx['tags']]
 
 249             current_books = self.get_queryset().values_list('book', flat=True).distinct()
 
 250             containing_books = Book.objects.filter(Q(id__in=current_books) | Q(children__in=current_books))
 
 252             related_tags.extend(list(
 
 253                 Tag.objects.usage_for_queryset(
 
 255                 ).exclude(category='set').exclude(pk__in=tag_ids)
 
 257             if self.request.user.is_authenticated:
 
 258                 related_tags.extend(list(
 
 259                     Tag.objects.usage_for_queryset(
 
 262                         user=self.request.user
 
 263                     ).exclude(name='').exclude(pk__in=tag_ids)
 
 266             related_tags = list(get_top_level_related_tags(self.ctx['tags']))
 
 267             if self.request.user.is_authenticated:
 
 268                 qs = Book.tagged.with_all(self.ctx['tags']).filter(findable=True)
 
 269                 related_tags.extend(list(
 
 270                     Tag.objects.usage_for_queryset(
 
 273                         user=self.request.user
 
 274                     ).exclude(name='').exclude(pk__in=tag_ids)
 
 277             fragments = Fragment.objects.filter(
 
 278                 Q(book__in=queryset) | Q(book__ancestor__in=queryset)
 
 281                 Tag.objects.usage_for_queryset(
 
 282                     fragments, counts=True
 
 283                 ).filter(category__in=('theme', 'object')).exclude(pk__in=tag_ids)
 
 284             .only('name', 'sort_key', 'category', 'slug'))
 
 290 def object_list(request, objects, fragments=None, related_tags=None, tags=None,
 
 291                 list_type='books', extra=None):
 
 294     tag_ids = [tag.pk for tag in tags]
 
 296     related_tag_lists = []
 
 298         related_tag_lists.append(related_tags)
 
 300         related_tag_lists.append(
 
 301             Tag.objects.usage_for_queryset(
 
 303             ).exclude(category='set').exclude(pk__in=tag_ids))
 
 304         if request.user.is_authenticated:
 
 305             related_tag_lists.append(
 
 306                 Tag.objects.usage_for_queryset(
 
 310                 ).exclude(name='').exclude(pk__in=tag_ids)
 
 312     if not (extra and extra.get('theme_is_set')):
 
 313         if fragments is None:
 
 314             if list_type == 'gallery':
 
 315                 fragments = PictureArea.objects.filter(picture__in=objects)
 
 317                 fragments = Fragment.objects.filter(book__in=objects)
 
 318         related_tag_lists.append(
 
 319             Tag.objects.usage_for_queryset(
 
 320                 fragments, counts=True
 
 321             ).filter(category='theme').exclude(pk__in=tag_ids)
 
 322             .only('name', 'sort_key', 'category', 'slug'))
 
 323         if isinstance(objects, QuerySet):
 
 324             objects = prefetch_relations(objects, 'author')
 
 327     categories = split_tags(*related_tag_lists)
 
 329     for c in ['set', 'author', 'epoch', 'kind', 'genre']:
 
 330         suggest.extend(sorted(categories[c], key=lambda t: -t.count))
 
 332     objects = list(objects)
 
 334     if not objects and len(tags) == 1 and list_type == 'books':
 
 335         if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
 
 336             return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
 
 339         best = random.sample(objects, 3)
 
 344         'object_list': objects,
 
 345         'categories': categories,
 
 347         'list_type': list_type,
 
 349         'main_tag': tags[0] if tags else None,
 
 351         'formats_form': forms.DownloadFormatsForm(),
 
 353         'active_menu_item': list_type,
 
 358     has_theme = any(((theme := x).category == 'theme' for x in tags))
 
 360         result['main_tag'] = theme
 
 361         template = 'catalogue/theme_detail.html'
 
 363         template = 'catalogue/author_detail.html'
 
 366         request, template, result,
 
 370 class ResponseInstead(Exception):
 
 371     def __init__(self, response):
 
 372         super(ResponseInstead, self).__init__()
 
 373         self.response = response
 
 376 def analyse_tags(request, tag_str):
 
 378         tags = Tag.get_tag_list(tag_str)
 
 379     except Tag.DoesNotExist:
 
 380         # Perhaps the user is asking about an author in Public Domain
 
 381         # counter (they are not represented in tags)
 
 382         chunks = tag_str.split('/')
 
 383         if len(chunks) == 2 and chunks[0] == 'autor':
 
 384             raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
 
 386     except Tag.MultipleObjectsReturned as e:
 
 387         # Ask the user to disambiguate
 
 388         raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
 
 389     except Tag.UrlDeprecationWarning as e:
 
 390         raise ResponseInstead(HttpResponsePermanentRedirect(
 
 391             reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
 
 396         if len(tags) > settings.MAX_TAG_LIST:
 
 398     except AttributeError:
 
 404 def theme_list(request, tags, list_type):
 
 405     shelf_tags = [tag for tag in tags if tag.category == 'set']
 
 406     fragment_tags = [tag for tag in tags if tag.category != 'set']
 
 407     if list_type == 'gallery':
 
 408         fragments = PictureArea.tagged.with_all(fragment_tags)
 
 410         fragments = Fragment.tagged.with_all(fragment_tags)
 
 413         # TODO: Pictures on shelves not supported yet.
 
 414         books = Book.tagged.with_all(shelf_tags).order_by()
 
 415         fragments = fragments.filter(Q(book__in=books) | Q(book__ancestor__in=books))
 
 416     elif list_type == 'books':
 
 417         fragments = fragments.filter(book__findable=True)
 
 419     if not fragments and len(tags) == 1 and list_type == 'books':
 
 420         if PictureArea.tagged.with_any(tags).exists() or Picture.tagged.with_any(tags).exists():
 
 421             return redirect('tagged_object_list_gallery', '/'.join(tag.url_chunk for tag in tags))
 
 423     return object_list(request, fragments, tags=tags, list_type=list_type, extra={
 
 424         'theme_is_set': True,
 
 425         'active_menu_item': 'theme',
 
 429 def tagged_object_list(request, tags, list_type):
 
 430     if list_type in ('books', 'audiobooks'):
 
 431         return TaggedObjectList.as_view()(request, tags=tags)
 
 434         tags = analyse_tags(request, tags)
 
 435     except ResponseInstead as e:
 
 438     if list_type == 'gallery' and any(tag.category == 'set' for tag in tags):
 
 441     if any(tag.category in ('theme', 'thing') for tag in tags):
 
 442         return theme_list(request, tags, list_type=list_type)
 
 444     if list_type == 'books':
 
 445         books = Book.tagged.with_all(tags)
 
 447         if any(tag.category == 'set' for tag in tags):
 
 448             params = {'objects': books}
 
 450             books = books.filter(findable=True)
 
 452                 'objects': Book.tagged_top_level(tags).filter(findable=True),
 
 453                 'fragments': Fragment.objects.filter(book__in=books),
 
 454                 'related_tags': get_top_level_related_tags(tags),
 
 456     elif list_type == 'gallery':
 
 457         params = {'objects': Picture.tagged.with_all(tags)}
 
 458     elif list_type == 'audiobooks':
 
 459         audiobooks = Book.objects.filter(findable=True, media__type__in=('mp3', 'ogg')).distinct()
 
 461             'objects': Book.tagged.with_all(tags, audiobooks),
 
 463                 'daisy': Book.tagged.with_all(
 
 464                     tags, audiobooks.filter(media__type='daisy').distinct()
 
 471     return object_list(request, tags=tags, list_type=list_type, **params)
 
 474 def book_fragments(request, slug, theme_slug):
 
 475     book = get_object_or_404(Book, slug=slug)
 
 476     theme = get_object_or_404(Tag, slug=theme_slug, category='theme')
 
 477     fragments = Fragment.tagged.with_all([theme]).filter(
 
 478         Q(book=book) | Q(book__ancestor=book))
 
 480     template_name = 'catalogue/book_fragments.html'
 
 487             'fragments': fragments,
 
 488             'active_menu_item': 'books',
 
 493 def book_detail(request, slug):
 
 495         book = Book.objects.get(slug=slug)
 
 496     except Book.DoesNotExist:
 
 497         return pdcounter_views.book_stub_detail(request, slug)
 
 501         'catalogue/book_detail.html',
 
 504             'accessible': book.is_accessible_to(request.user),
 
 505             'book_children': book.children.all().order_by('parent_number', 'sort_key'),
 
 506             'active_menu_item': 'books',
 
 507             'club': Club.objects.first() if book.preview else None,
 
 508             'donation_form': DonationStep1Form(),
 
 512 def book_text(request, slug):
 
 513     book = get_object_or_404(Book, slug=slug)
 
 515     if not book.is_accessible_to(request.user):
 
 516         return HttpResponseRedirect(book.get_absolute_url())
 
 518     if not book.has_html_file():
 
 520     with book.html_file.open('r') as f:
 
 523     return render(request, 'catalogue/book_text.html', {
 
 525         'extra_info': book.get_extra_info_json(),
 
 526         'book_text': book_text,
 
 527         'inserts': DynamicTextInsert.get_all(request),
 
 529         'club': Club.objects.first(),
 
 530         'donation_form': DonationStep1Form(),
 
 539 def import_book(request):
 
 540     """docstring for import_book"""
 
 541     book_import_form = forms.BookImportForm(request.POST, request.FILES)
 
 542     if book_import_form.is_valid():
 
 544             book_import_form.save()
 
 549             info = sys.exc_info()
 
 550             exception = pprint.pformat(info[1])
 
 551             tb = '\n'.join(traceback.format_tb(info[2]))
 
 553                 "Błąd: %(exception)s\n\n%(tb)s" % {
 
 554                     'exception': exception, 'tb': tb
 
 556                 content_type='text/plain'
 
 558         return HttpResponse("Książka zaimportowana")
 
 559     return HttpResponse("Błąd podczas importowania pliku: %r" % book_import_form.errors)
 
 564 def book_info(request, book_id, lang='pl'):
 
 565     book = get_object_or_404(Book, id=book_id)
 
 566     # set language by hand
 
 567     translation.activate(lang)
 
 568     return render(request, 'catalogue/book_info.html', {'book': book})
 
 571 def tag_info(request, tag_id):
 
 572     tag = get_object_or_404(Tag, id=tag_id)
 
 573     return HttpResponse(tag.description)
 
 577 def embargo_link(request, key, format_, slug):
 
 578     book = get_object_or_404(Book, slug=slug)
 
 579     if format_ not in Book.formats:
 
 581     if key != book.preview_key:
 
 583     media_file = book.get_media(format_)
 
 585         return HttpResponseRedirect(media_file.url)
 
 586     return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_])
 
 589 def download_zip(request, file_format=None, media_format=None, slug=None):
 
 591         url = Book.zip_format(file_format)
 
 592     elif media_format and slug is not None:
 
 593         book = get_object_or_404(Book, slug=slug)
 
 594         url = book.zip_audiobooks(media_format)
 
 596         raise Http404('No format specified for zip package')
 
 597     return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
 
 600 class CustomPDFFormView(AjaxableFormView):
 
 601     form_class = forms.CustomPDFForm
 
 602     title = gettext_lazy('Stwórz własny PDF')
 
 603     submit = gettext_lazy('Pobierz')
 
 604     template = 'catalogue/custom_pdf_form.html'
 
 607     def __call__(self, *args, **kwargs):
 
 608         if settings.NO_CUSTOM_PDF:
 
 609             raise Http404('Custom PDF is disabled')
 
 610         return super(CustomPDFFormView, self).__call__(*args, **kwargs)
 
 612     def form_args(self, request, obj):
 
 613         """Override to parse view args and give additional args to the form."""
 
 616     def validate_object(self, obj, request):
 
 618         if not book.is_accessible_to(request.user):
 
 619             return HttpResponseRedirect(book.get_absolute_url())
 
 620         return super(CustomPDFFormView, self).validate_object(obj, request)
 
 622     def get_object(self, request, slug, *args, **kwargs):
 
 623         book = get_object_or_404(Book, slug=slug)
 
 626     def context_description(self, request, obj):
 
 627         return obj.pretty_title()
 
 630 def tag_catalogue(request, category):
 
 631     if category == 'theme':
 
 632         tags = Tag.objects.usage_for_model(
 
 633             Fragment, counts=True).filter(category='theme')
 
 635         tags = list(get_top_level_related_tags((), categories=(category,)))
 
 637     described_tags = [tag for tag in tags if tag.description]
 
 639     if len(described_tags) > 4:
 
 640         best = random.sample(described_tags, 4)
 
 642         best = described_tags
 
 644     template_name = 'catalogue/tag_catalogue.html'
 
 645     return render(request, template_name, {
 
 648         'title': constants.CATEGORIES_NAME_PLURAL[category],
 
 649         'whole_category': constants.WHOLE_CATEGORY[category],
 
 650         'active_menu_item': 'theme' if category == 'theme' else None,
 
 654 def collections(request):
 
 655     objects = Collection.objects.filter(listed=True)
 
 658         best = random.sample(list(objects), 4)
 
 662     template_name = 'catalogue/collections.html'
 
 663     return render(request, template_name, {
 
 666         'active_menu_item': 'collections'
 
 670 def ridero_cover(request, slug):
 
 671     from librarian.cover import make_cover
 
 672     wldoc = Book.objects.get(slug=slug).wldocument()
 
 673     cover = make_cover(wldoc.book_info, width=980, bleed=20, format='PNG')
 
 674     response = HttpResponse(content_type="image/png")
 
 679 def get_isbn(request, book_format, slug):
 
 680     book = Book.objects.get(slug=slug)
 
 681     return HttpResponse(book.get_extra_info_json().get('isbn_%s' % book_format))