X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/af544a7534db40bd650ac88fe5cf23cccf7b5b27..71209be8f9c399340bddb819f71e99ecf116187b:/apps/catalogue/views.py diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index b69aa4a9c..9044a809e 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -2,114 +2,56 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -import tempfile -import zipfile -import tarfile -import sys -import pprint -import traceback import re import itertools -from operator import itemgetter -from datetime import datetime from django.conf import settings from django.template import RequestContext from django.shortcuts import render_to_response, get_object_or_404 -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect from django.core.urlresolvers import reverse from django.db.models import Q from django.contrib.auth.decorators import login_required, user_passes_test from django.utils.datastructures import SortedDict -from django.views.decorators.http import require_POST -from django.contrib import auth -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.utils import simplejson -from django.utils.functional import Promise -from django.utils.encoding import force_unicode from django.utils.http import urlquote_plus -from django.views.decorators import cache +from django.utils import translation from django.utils.translation import ugettext as _ -from django.views.generic.list_detail import object_list -from django.template.defaultfilters import slugify + +from ajaxable.utils import JSONResponse, AjaxableFormView + from catalogue import models from catalogue import forms -from catalogue.utils import split_tags -from newtagging import views as newtagging_views +from catalogue.utils import (split_tags, AttachmentHttpResponse, + async_build_pdf, MultiQuerySet) from pdcounter import models as pdcounter_models from pdcounter import views as pdcounter_views -from slughifi import slughifi +from suggest.forms import PublishingSuggestForm +from picture.models import Picture +from os import path staff_required = user_passes_test(lambda user: user.is_staff) -class LazyEncoder(simplejson.JSONEncoder): - def default(self, obj): - if isinstance(obj, Promise): - return force_unicode(obj) - return obj - -# shortcut for JSON reponses -class JSONResponse(HttpResponse): - def __init__(self, data={}, callback=None, **kwargs): - # get rid of mimetype - kwargs.pop('mimetype', None) - data = simplejson.dumps(data) - if callback: - data = callback + "(" + data + ");" - super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs) - - -def main_page(request): - if request.user.is_authenticated(): - shelves = models.Tag.objects.filter(category='set', user=request.user) - new_set_form = forms.NewSetForm() - - tags = models.Tag.objects.exclude(category__in=('set', 'book')) +def catalogue(request): + tags = models.Tag.objects.exclude( + category__in=('set', 'book')).exclude(book_count=0) + tags = list(tags) for tag in tags: - tag.count = tag.get_count() + tag.count = tag.book_count categories = split_tags(tags) fragment_tags = categories.get('theme', []) - form = forms.SearchForm() - return render_to_response('catalogue/main_page.html', locals(), + return render_to_response('catalogue/catalogue.html', locals(), context_instance=RequestContext(request)) -def book_list(request, filter=None, template_name='catalogue/book_list.html'): +def book_list(request, filter=None, template_name='catalogue/book_list.html', + context=None): """ generates a listing of all books, optionally filtered with a test function """ - form = forms.SearchForm() - - books_by_parent = {} - books = models.Book.objects.all().order_by('parent_number', 'title').only('title', 'parent', 'slug') - if filter: - books = books.filter(filter).distinct() - book_ids = set((book.pk for book in books)) - for book in books: - parent = book.parent_id - if parent not in book_ids: - parent = None - books_by_parent.setdefault(parent, []).append(book) - else: - for book in books: - books_by_parent.setdefault(book.parent_id, []).append(book) - - orphans = [] - books_by_author = SortedDict() + books_by_author, orphans, books_by_parent = models.Book.book_list(filter) books_nav = SortedDict() - for tag in models.Tag.objects.filter(category='author'): - books_by_author[tag] = [] - - for book in books_by_parent.get(None,()): - authors = list(book.tags.filter(category='author')) - if authors: - for author in authors: - books_by_author[author].append(book) - else: - orphans.append(book) - for tag in books_by_author: if books_by_author[tag]: books_nav.setdefault(tag.sort_key[0], []).append(tag) @@ -119,15 +61,26 @@ def book_list(request, filter=None, template_name='catalogue/book_list.html'): def audiobook_list(request): - return book_list(request, Q(medias__type='mp3') | Q(medias__type='ogg'), + return book_list(request, Q(media__type='mp3') | Q(media__type='ogg'), template_name='catalogue/audiobook_list.html') def daisy_list(request): - return book_list(request, Q(medias__type='daisy'), + return book_list(request, Q(media__type='daisy'), template_name='catalogue/daisy_list.html') +def collection(request, slug): + coll = get_object_or_404(models.Collection, slug=slug) + slugs = coll.book_slugs.split() + # allow URIs + slugs = [slug.rstrip('/').rsplit('/', 1)[-1] if '/' in slug else slug + for slug in slugs] + return book_list(request, Q(slug__in=slugs), + template_name='catalogue/collection.html', + context={'collection': coll}) + + def differentiate_tags(request, tags, ambiguous_slugs): beginning = '/'.join(tag.url_chunk for tag in tags) unparsed = '/'.join(ambiguous_slugs[1:]) @@ -153,6 +106,8 @@ def tagged_object_list(request, tags=''): raise Http404 except models.Tag.MultipleObjectsReturned, e: return differentiate_tags(request, e.tags, e.ambiguous_slugs) + except models.Tag.UrlDeprecationWarning, e: + return HttpResponsePermanentRedirect(reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])) try: if len(tags) > settings.MAX_TAG_LIST: @@ -194,14 +149,10 @@ def tagged_object_list(request, tags=''): objects = fragments else: - # get relevant books and their tags - objects = models.Book.tagged.with_all(tags) - if not shelf_is_set: - # eliminate descendants - l_tags = models.Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects]) - descendants_keys = [book.pk for book in models.Book.tagged.with_any(l_tags)] - if descendants_keys: - objects = objects.exclude(pk__in=descendants_keys) + if shelf_is_set: + objects = models.Book.tagged.with_all(tags) + else: + objects = models.Book.tagged_top_level(tags) # get related tags from `tag_counter` and `theme_counter` related_counts = {} @@ -223,29 +174,29 @@ def tagged_object_list(request, tags=''): only_author = len(tags) == 1 and tags[0].category == 'author' objects = models.Book.objects.none() - return object_list( - request, - objects, - template_name='catalogue/tagged_object_list.html', - extra_context={ + # Add pictures + objects = MultiQuerySet(Picture.tagged.with_all(tags), objects) + + return render_to_response('catalogue/tagged_object_list.html', + { + 'object_list': objects, 'categories': categories, 'only_shelf': only_shelf, 'only_author': only_author, 'only_my_shelf': only_my_shelf, 'formats_form': forms.DownloadFormatsForm(), - 'tags': tags, - } - ) + }, + context_instance=RequestContext(request)) -def book_fragments(request, book_slug, theme_slug): - book = get_object_or_404(models.Book, slug=book_slug) - book_tag = get_object_or_404(models.Tag, slug='l-' + book_slug, category='book') +def book_fragments(request, slug, theme_slug): + book = get_object_or_404(models.Book, slug=slug) + + book_tag = book.book_tag() theme = get_object_or_404(models.Tag, slug=theme_slug, category='theme') fragments = models.Fragment.tagged.with_all([book_tag, theme]) - form = forms.SearchForm() return render_to_response('catalogue/book_fragments.html', locals(), context_instance=RequestContext(request)) @@ -256,32 +207,52 @@ def book_detail(request, slug): except models.Book.DoesNotExist: return pdcounter_views.book_stub_detail(request, slug) - book_tag = book.book_tag() - tags = list(book.tags.filter(~Q(category='set'))) - categories = split_tags(tags) - book_children = book.children.all().order_by('parent_number', 'title') - - _book = book - parents = [] - while _book.parent: - parents.append(_book.parent) - _book = _book.parent - parents = reversed(parents) - - theme_counter = book.theme_counter - book_themes = models.Tag.objects.filter(pk__in=theme_counter.keys()) - for tag in book_themes: - tag.count = theme_counter[tag.pk] - - extra_info = book.get_extra_info_value() - - form = forms.SearchForm() + book_children = book.children.all().order_by('parent_number', 'sort_key') return render_to_response('catalogue/book_detail.html', locals(), context_instance=RequestContext(request)) +def player(request, slug): + book = get_object_or_404(models.Book, slug=slug) + if not book.has_media('mp3'): + raise Http404 + + ogg_files = {} + for m in book.media.filter(type='ogg').order_by(): + ogg_files[m.name] = m + + audiobooks = [] + have_oggs = True + projects = set() + for mp3 in book.media.filter(type='mp3'): + # ogg files are always from the same project + meta = mp3.get_extra_info_value() + project = meta.get('project') + if not project: + # temporary fallback + project = u'CzytamySłuchając' + + projects.add((project, meta.get('funded_by', ''))) + + media = {'mp3': mp3} + + ogg = ogg_files.get(mp3.name) + if ogg: + media['ogg'] = ogg + else: + have_oggs = False + audiobooks.append(media) + print audiobooks + + projects = sorted(projects) + + return render_to_response('catalogue/player.html', locals(), + context_instance=RequestContext(request)) + + def book_text(request, slug): book = get_object_or_404(models.Book, slug=slug) + if not book.has_html_file(): raise Http404 book_themes = {} @@ -413,7 +384,7 @@ def books_starting_with(prefix): def find_best_matches(query, user=None): - """ Finds a Book, Tag, BookStub or Author best matching a query. + """ Finds a models.Book, Tag, models.BookStub or Author best matching a query. Returns a with: - zero elements when nothing is found, @@ -467,7 +438,9 @@ def search(request): {'tags':tag_list, 'prefix':prefix, 'results':((x, _get_result_link(x, tag_list), _get_result_type(x)) for x in result)}, context_instance=RequestContext(request)) else: - return render_to_response('catalogue/search_no_hits.html', {'tags':tag_list, 'prefix':prefix}, + form = PublishingSuggestForm(initial={"books": prefix + ", "}) + return render_to_response('catalogue/search_no_hits.html', + {'tags':tag_list, 'prefix':prefix, "pubsuggest_form": form}, context_instance=RequestContext(request)) @@ -477,7 +450,7 @@ def tags_starting_with(request): if len(prefix) < 2: return HttpResponse('') tags_list = [] - result = "" + result = "" for tag in _tags_starting_with(prefix, request.user): if not tag.name in tags_list: result += "\n" + tag.name @@ -492,240 +465,14 @@ def json_tags_starting_with(request, callback=None): if len(prefix) < 2: return HttpResponse('') tags_list = [] - result = "" for tag in _tags_starting_with(prefix, request.user): if not tag.name in tags_list: - result += "\n" + tag.name tags_list.append(tag.name) - dict_result = {"matches": tags_list} - return JSONResponse(dict_result, callback) - -# ==================== -# = Shelf management = -# ==================== -@login_required -@cache.never_cache -def user_shelves(request): - shelves = models.Tag.objects.filter(category='set', user=request.user) - new_set_form = forms.NewSetForm() - return render_to_response('catalogue/user_shelves.html', locals(), - context_instance=RequestContext(request)) - -@cache.never_cache -def book_sets(request, slug): - if not request.user.is_authenticated(): - return HttpResponse(_('

To maintain your shelves you need to be logged in.

')) - - book = get_object_or_404(models.Book, slug=slug) - user_sets = models.Tag.objects.filter(category='set', user=request.user) - book_sets = book.tags.filter(category='set', user=request.user) - - if request.method == 'POST': - form = forms.ObjectSetsForm(book, request.user, request.POST) - if form.is_valid(): - old_shelves = list(book.tags.filter(category='set')) - new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] - - for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]: - shelf.book_count = None - shelf.save() - - for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]: - shelf.book_count = None - shelf.save() - - book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user))) - if request.is_ajax(): - return JSONResponse('{"msg":"'+_("

Shelves were sucessfully saved.

")+'", "after":"close"}') - else: - return HttpResponseRedirect('/') - else: - form = forms.ObjectSetsForm(book, request.user) - new_set_form = forms.NewSetForm() - - return render_to_response('catalogue/book_sets.html', locals(), - context_instance=RequestContext(request)) - - -@login_required -@require_POST -@cache.never_cache -def remove_from_shelf(request, shelf, book): - book = get_object_or_404(models.Book, slug=book) - shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user) - - if shelf in book.tags: - models.Tag.objects.remove_tag(book, shelf) - - shelf.book_count = None - shelf.save() - - return HttpResponse(_('Book was successfully removed from the shelf')) + if request.GET.get('mozhint', ''): + result = [prefix, tags_list] else: - return HttpResponse(_('This book is not on the shelf')) - - -def collect_books(books): - """ - Returns all real books in collection. - """ - result = [] - for book in books: - if len(book.children.all()) == 0: - result.append(book) - else: - result += collect_books(book.children.all()) - return result - - -@cache.never_cache -def download_shelf(request, slug): - """" - Create a ZIP archive on disk and transmit it in chunks of 8KB, - without loading the whole file into memory. A similar approach can - be used for large dynamic PDF files. - """ - shelf = get_object_or_404(models.Tag, slug=slug, category='set') - - formats = [] - form = forms.DownloadFormatsForm(request.GET) - if form.is_valid(): - formats = form.cleaned_data['formats'] - if len(formats) == 0: - formats = ['pdf', 'epub', 'odt', 'txt', 'mp3', 'ogg', 'daisy'] - - # Create a ZIP archive - temp = tempfile.TemporaryFile() - archive = zipfile.ZipFile(temp, 'w') - - already = set() - for book in collect_books(models.Book.tagged.with_all(shelf)): - if 'pdf' in formats and book.pdf_file: - filename = book.pdf_file.path - archive.write(filename, str('%s.pdf' % book.slug)) - if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file: - filename = book.root_ancestor.epub_file.path - archive.write(filename, str('%s.epub' % book.root_ancestor.slug)) - already.add(book.root_ancestor) - if 'odt' in formats and book.has_media("odt"): - for file in book.get_media("odt"): - filename = file.file.path - archive.write(filename, str('%s.odt' % slugify(file.name))) - if 'txt' in formats and book.txt_file: - filename = book.txt_file.path - archive.write(filename, str('%s.txt' % book.slug)) - if 'mp3' in formats and book.has_media("mp3"): - for file in book.get_media("mp3"): - filename = file.file.path - archive.write(filename, str('%s.mp3' % slugify(file.name))) - if 'ogg' in formats and book.has_media("ogg"): - for file in book.get_media("ogg"): - filename = file.file.path - archive.write(filename, str('%s.ogg' % slugify(file.name))) - if 'daisy' in formats and book.has_media("daisy"): - for file in book.get_media("daisy"): - filename = file.file.path - archive.write(filename, str('%s.daisy' % slugify(file.name))) - archive.close() - - response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed') - response['Content-Disposition'] = 'attachment; filename=%s.zip' % slughifi(shelf.name) - response['Content-Length'] = temp.tell() - - temp.seek(0) - response.write(temp.read()) - return response - - -@cache.never_cache -def shelf_book_formats(request, shelf): - """" - Returns a list of formats of books in shelf. - """ - shelf = get_object_or_404(models.Tag, slug=shelf, category='set') - - formats = {'pdf': False, 'epub': False, 'odt': False, 'txt': False, 'mp3': False, 'ogg': False, 'daisy': False} - - for book in collect_books(models.Book.tagged.with_all(shelf)): - if book.pdf_file: - formats['pdf'] = True - if book.root_ancestor.epub_file: - formats['epub'] = True - if book.txt_file: - formats['txt'] = True - for format in ('odt', 'mp3', 'ogg'): - if not formats[format] and book.has_media(format): - formats[format] = True - - return HttpResponse(LazyEncoder().encode(formats)) - - -@login_required -@require_POST -@cache.never_cache -def new_set(request): - new_set_form = forms.NewSetForm(request.POST) - if new_set_form.is_valid(): - new_set = new_set_form.save(request.user) - - if request.is_ajax(): - return JSONResponse('{"id":"%d", "name":"%s", "msg":"

Shelf %s was successfully created

"}' % (new_set.id, new_set.name, new_set)) - else: - return HttpResponseRedirect('/') - - return HttpResponseRedirect('/') - - -@login_required -@require_POST -@cache.never_cache -def delete_shelf(request, slug): - user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user) - user_set.delete() - - if request.is_ajax(): - return HttpResponse(_('

Shelf %s was successfully removed

') % user_set.name) - else: - return HttpResponseRedirect('/') - - -# ================== -# = Authentication = -# ================== -@require_POST -@cache.never_cache -def login(request): - form = AuthenticationForm(data=request.POST, prefix='login') - if form.is_valid(): - auth.login(request, form.get_user()) - response_data = {'success': True, 'errors': {}} - else: - response_data = {'success': False, 'errors': form.errors} - return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) - - -@require_POST -@cache.never_cache -def register(request): - registration_form = UserCreationForm(request.POST, prefix='registration') - if registration_form.is_valid(): - user = registration_form.save() - user = auth.authenticate( - username=registration_form.cleaned_data['username'], - password=registration_form.cleaned_data['password1'] - ) - auth.login(request, user) - response_data = {'success': True, 'errors': {}} - else: - response_data = {'success': False, 'errors': registration_form.errors} - return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) - - -@cache.never_cache -def logout_then_redirect(request): - auth.logout(request) - return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) - + result = {"matches": tags_list} + return JSONResponse(result, callback) # ========= @@ -740,6 +487,9 @@ def import_book(request): try: book_import_form.save() except: + import sys + import pprint + import traceback info = sys.exc_info() exception = pprint.pformat(info[1]) tb = '\n'.join(traceback.format_tb(info[2])) @@ -749,52 +499,65 @@ def import_book(request): return HttpResponse(_("Error importing file: %r") % book_import_form.errors) +# info views for API -def clock(request): - """ Provides server time for jquery.countdown, - in a format suitable for Date.parse() - """ - return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S')) +def book_info(request, id, lang='pl'): + book = get_object_or_404(models.Book, id=id) + # set language by hand + translation.activate(lang) + return render_to_response('catalogue/book_info.html', locals(), + context_instance=RequestContext(request)) -@cache.never_cache -def xmls(request): - """" - Create a zip archive with all XML files. - """ - temp = tempfile.TemporaryFile() - archive = zipfile.ZipFile(temp, 'w') +def tag_info(request, id): + tag = get_object_or_404(models.Tag, id=id) + return HttpResponse(tag.description) - for book in models.Book.objects.all(): - archive.write(book.xml_file.path, str('%s.xml' % book.slug)) - archive.close() - response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed') - response['Content-Disposition'] = 'attachment; filename=xmls.zip' - response['Content-Length'] = temp.tell() +def download_zip(request, format, slug=None): + url = None + if format in models.Book.ebook_formats: + url = models.Book.zip_format(format) + elif format in ('mp3', 'ogg') and slug is not None: + book = get_object_or_404(models.Book, slug=slug) + url = book.zip_audiobooks(format) + else: + raise Http404('No format specified for zip package') + return HttpResponseRedirect(urlquote_plus(settings.MEDIA_URL + url, safe='/?=')) - temp.seek(0) - response.write(temp.read()) - return response +def download_custom_pdf(request, slug, method='GET'): + book = get_object_or_404(models.Book, slug=slug) -@cache.never_cache -def epubs(request): - """" - Create a tar archive with all EPUB files, segregated to directories. - """ + if request.method == method: + form = forms.CustomPDFForm(method == 'GET' and request.GET or request.POST) + if form.is_valid(): + cust = form.customizations + pdf_file = models.get_customized_pdf_path(book, cust) + + if not path.exists(pdf_file): + result = async_build_pdf.delay(book.id, cust, pdf_file) + result.wait() + return AttachmentHttpResponse(file_name=("%s.pdf" % book.slug), file_path=pdf_file, mimetype="application/pdf") + else: + raise Http404(_('Incorrect customization options for PDF')) + else: + raise Http404(_('Bad method')) - temp = tempfile.TemporaryFile() - archive = tarfile.TarFile(fileobj=temp, mode='w') - for book in models.Book.objects.exclude(epub_file=''): - archive.add(book.epub_file.path, (u'%s/%s.epub' % (book.get_extra_info_value()['author'], book.slug)).encode('utf-8')) - archive.close() +class CustomPDFFormView(AjaxableFormView): + form_class = forms.CustomPDFForm + title = _('Download custom PDF') + submit = _('Download') - response = HttpResponse(content_type='application/tar', mimetype='application/x-tar') - response['Content-Disposition'] = 'attachment; filename=epubs.tar' - response['Content-Length'] = temp.tell() + def __call__(self, request): + from copy import copy + if request.method == 'POST': + request.GET = copy(request.GET) + request.GET['next'] = "%s?%s" % (reverse('catalogue.views.download_custom_pdf', args=[request.GET['slug']]), + request.POST.urlencode()) + return super(CustomPDFFormView, self).__call__(request) - temp.seek(0) - response.write(temp.read()) - return response + + def success(self, *args): + pass