''' the good stuff! the books! ''' from django.contrib.auth.decorators import login_required, permission_required from django.contrib.postgres.search import SearchRank, SearchVector from django.core.paginator import Paginator from django.db import transaction from django.db.models import Avg, Q from django.http import HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.connectors import connector_manager from bookwyrm.settings import PAGE_LENGTH from .helpers import is_api_request, get_activity_feed, get_edition from .helpers import privacy_filter # pylint: disable= no-self-use class Book(View): ''' a book! this is the stuff ''' def get(self, request, book_id): ''' info about a book ''' try: page = int(request.GET.get('page', 1)) except ValueError: page = 1 try: book = models.Book.objects.select_subclasses().get(id=book_id) except models.Book.DoesNotExist: return HttpResponseNotFound() if is_api_request(request): return ActivitypubResponse(book.to_activity()) if isinstance(book, models.Work): book = book.get_default_edition() if not book: return HttpResponseNotFound() work = book.parent_work if not work: return HttpResponseNotFound() # all reviews for the book reviews = models.Review.objects.filter(book__in=work.editions.all()) reviews = get_activity_feed(request.user, queryset=reviews) # the reviews to show paginated = Paginator(reviews.exclude( Q(content__isnull=True) | Q(content='') ), PAGE_LENGTH) reviews_page = paginated.page(page) user_tags = readthroughs = user_shelves = other_edition_shelves = [] if request.user.is_authenticated: user_tags = models.UserTag.objects.filter( book=book, user=request.user ).values_list('tag__identifier', flat=True) readthroughs = models.ReadThrough.objects.filter( user=request.user, book=book, ).order_by('start_date') for readthrough in readthroughs: readthrough.progress_updates = \ readthrough.progressupdate_set.all() \ .order_by('-updated_date') user_shelves = models.ShelfBook.objects.filter( user=request.user, book=book ) other_edition_shelves = models.ShelfBook.objects.filter( ~Q(book=book), user=request.user, book__parent_work=book.parent_work, ) data = { 'book': book, 'reviews': reviews_page, 'review_count': reviews.count(), 'ratings': reviews.filter(Q(content__isnull=True) | Q(content='')), 'rating': reviews.aggregate(Avg('rating'))['rating__avg'], 'tags': models.UserTag.objects.filter(book=book), 'lists': privacy_filter( request.user, book.list_set.filter(listitem__approved=True) ), 'user_tags': user_tags, 'user_shelves': user_shelves, 'other_edition_shelves': other_edition_shelves, 'readthroughs': readthroughs, 'path': '/book/%s' % book_id, } return TemplateResponse(request, 'book.html', data) @method_decorator(login_required, name='dispatch') @method_decorator( permission_required('bookwyrm.edit_book', raise_exception=True), name='dispatch') class EditBook(View): ''' edit a book ''' def get(self, request, book_id=None): ''' info about a book ''' book = None if book_id: book = get_edition(book_id) if not book.description: book.description = book.parent_work.description data = { 'book': book, 'form': forms.EditionForm(instance=book) } return TemplateResponse(request, 'edit_book.html', data) def post(self, request, book_id=None): ''' edit a book cool ''' # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) data = { 'book': book, 'form': form } if not form.is_valid(): return TemplateResponse(request, 'edit_book.html', data) add_author = request.POST.get('add_author') # we're adding an author through a free text field if add_author: data['add_author'] = add_author # check for existing authors vector = SearchVector('name', weight='A') +\ SearchVector('aliases', weight='B') data['author_matches'] = models.Author.objects.annotate( search=vector ).annotate( rank=SearchRank(vector, add_author) ).filter(rank__gt=0.8).order_by('-rank')[:5] # we're creating a new book if not book: # check if this is an edition of an existing work author_text = book.author_text if book else add_author data['book_matches'] = connector_manager.local_search( '%s %s' % (form.cleaned_data.get('title'), author_text), min_confidence=0.5, raw=True )[:5] # either of the above cases requires additional confirmation if add_author or not book: # creting a book or adding an author to a book needs another step data['confirm_mode'] = True return TemplateResponse(request, 'edit_book.html', data) remove_authors = request.POST.getlist('remove_authors') for author_id in remove_authors: book.authors.remove(author_id) book = form.save() return redirect('/book/%s' % book.id) @method_decorator(login_required, name='dispatch') @method_decorator( permission_required('bookwyrm.edit_book', raise_exception=True), name='dispatch') class ConfirmEditBook(View): ''' confirm edits to a book ''' def post(self, request, book_id=None): ''' edit a book cool ''' # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) data = { 'book': book, 'form': form } if not form.is_valid(): return TemplateResponse(request, 'edit_book.html', data) # create work, if needed # TODO # save book book = form.save() # get or create author as needed if request.POST.get('add_author'): if request.POST.get('author_match'): author = get_object_or_404( models.Author, id=request.POST['author_match']) else: author = models.Author.objects.create( name=request.POST.get('add_author')) book.authors.add(author) remove_authors = request.POST.getlist('remove_authors') for author_id in remove_authors: book.authors.remove(author_id) return redirect('/book/%s' % book.id) class Editions(View): ''' list of editions ''' def get(self, request, book_id): ''' list of editions of a book ''' work = get_object_or_404(models.Work, id=book_id) if is_api_request(request): return ActivitypubResponse(work.to_edition_list(**request.GET)) data = { 'editions': work.editions.order_by('-edition_rank').all(), 'work': work, } return TemplateResponse(request, 'editions.html', data) @login_required @require_POST def upload_cover(request, book_id): ''' upload a new cover ''' book = get_object_or_404(models.Edition, id=book_id) form = forms.CoverForm(request.POST, request.FILES, instance=book) if not form.is_valid(): return redirect('/book/%d' % book.id) book.last_edited_by = request.user book.cover = form.files['cover'] book.save() return redirect('/book/%s' % book.id) @login_required @require_POST @permission_required('bookwyrm.edit_book', raise_exception=True) def add_description(request, book_id): ''' upload a new cover ''' if not request.method == 'POST': return redirect('/') book = get_object_or_404(models.Edition, id=book_id) description = request.POST.get('description') book.description = description book.last_edited_by = request.user book.save() return redirect('/book/%s' % book.id) @require_POST def resolve_book(request): ''' figure out the local path to a book from a remote_id ''' remote_id = request.POST.get('remote_id') connector = connector_manager.get_or_create_connector(remote_id) book = connector.get_or_create_book(remote_id) return redirect('/book/%d' % book.id) @login_required @require_POST @transaction.atomic def switch_edition(request): ''' switch your copy of a book to a different edition ''' edition_id = request.POST.get('edition') new_edition = get_object_or_404(models.Edition, id=edition_id) shelfbooks = models.ShelfBook.objects.filter( book__parent_work=new_edition.parent_work, shelf__user=request.user ) for shelfbook in shelfbooks.all(): with transaction.atomic(): models.ShelfBook.objects.create( created_date=shelfbook.created_date, user=shelfbook.user, shelf=shelfbook.shelf, book=new_edition ) shelfbook.delete() readthroughs = models.ReadThrough.objects.filter( book__parent_work=new_edition.parent_work, user=request.user ) for readthrough in readthroughs.all(): readthrough.book = new_edition readthrough.save() return redirect('/book/%d' % new_edition.id)