''' views for actions you can take in the application ''' from io import BytesIO, TextIOWrapper from PIL import Image from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.core.files.base import ContentFile from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import redirect from django.template.response import TemplateResponse from fedireads import forms, models, outgoing from fedireads import goodreads_import from fedireads.settings import DOMAIN from fedireads.views import get_user_from_username def user_login(request): ''' authenticate user login ''' if request.method == 'GET': return redirect('/login') register_form = forms.RegisterForm() login_form = forms.LoginForm(request.POST) if not login_form.is_valid(): return TemplateResponse( request, 'login.html', {'login_form': login_form, 'register_form': register_form} ) username = login_form.data['username'] username = '%s@%s' % (username, DOMAIN) password = login_form.data['password'] user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect(request.GET.get('next', '/')) return TemplateResponse( request, 'login.html', {'login_form': login_form, 'register_form': register_form} ) def register(request): ''' join the server ''' if request.method == 'GET': return redirect('/login') form = forms.RegisterForm(request.POST) if not form.is_valid(): return redirect('/register/') username = form.data['username'] email = form.data['email'] password = form.data['password'] user = models.User.objects.create_user(username, email, password) login(request, user) return redirect('/') @login_required def user_logout(request): ''' done with this place! outa here! ''' logout(request) return redirect('/') @login_required def edit_profile(request): ''' les get fancy with images ''' if not request.method == 'POST': return redirect('/user/%s' % request.user.localname) form = forms.EditUserForm(request.POST, request.FILES) if not form.is_valid(): return redirect('/') request.user.name = form.data['name'] if 'avatar' in form.files: # crop and resize avatar upload image = Image.open(form.files['avatar']) target_size = 120 width, height = image.size thumbnail_scale = height / (width / target_size) if height > width \ else width / (height / target_size) image.thumbnail([thumbnail_scale, thumbnail_scale]) width, height = image.size width_diff = width - target_size height_diff = height - target_size cropped = image.crop(( int(width_diff / 2), int(height_diff / 2), int(width - (width_diff / 2)), int(height - (height_diff / 2)) )) output = BytesIO() cropped.save(output, format=image.format) ContentFile(output.getvalue()) request.user.avatar.save( form.files['avatar'].name, ContentFile(output.getvalue()) ) request.user.summary = form.data['summary'] request.user.manually_approves_followers = \ form.cleaned_data['manually_approves_followers'] request.user.save() outgoing.handle_update_user(request.user) return redirect('/user/%s' % request.user.localname) def resolve_book(request): ''' figure out the local path to a book from a remote_id ''' remote_id = request.POST.get('remote_id') book = get_or_create_book(remote_id, key='remote_id') return redirect('/book/%d' % book.id) @login_required def edit_book(request, book_id): ''' edit a book cool ''' if not request.method == 'POST': return redirect('/book/%s' % request.user.localname) try: book = models.Edition.objects.get(id=book_id) except models.Edition.DoesNotExist: return HttpResponseNotFound() form = forms.EditionForm(request.POST, request.FILES, instance=book) if not form.is_valid(): return redirect(request.headers.get('Referer', '/')) form.save() outgoing.handle_update_book(request.user, book) return redirect('/book/%s' % book.id) @login_required def upload_cover(request, book_id): ''' upload a new cover ''' # TODO: alternate covers? if not request.method == 'POST': return redirect('/book/%s' % request.user.localname) try: book = models.Edition.objects.get(id=book_id) except models.Book.DoesNotExist: return HttpResponseNotFound() form = forms.CoverForm(request.POST, request.FILES, instance=book) if not form.is_valid(): return redirect(request.headers.get('Referer', '/')) book.cover = form.files['cover'] book.sync_cover = False book.save() outgoing.handle_update_book(request.user, book) return redirect('/book/%s' % book.id) @login_required def shelve(request): ''' put a on a user's shelf ''' book = models.Book.objects.select_subclasses().get(id=request.POST['book']) if isinstance(book, models.Work): book = book.default_edition desired_shelf = models.Shelf.objects.filter( identifier=request.POST['shelf'], user=request.user ).first() if request.POST.get('reshelve', True): try: current_shelf = models.Shelf.objects.get( user=request.user, edition=book ) outgoing.handle_unshelve(request.user, book, current_shelf) except models.Shelf.DoesNotExist: # this just means it isn't currently on the user's shelves pass outgoing.handle_shelve(request.user, book, desired_shelf) return redirect('/') @login_required def rate(request): ''' just a star rating for a book ''' form = forms.RatingForm(request.POST) book_id = request.POST.get('book') # TODO: better failure behavior if not form.is_valid(): return redirect('/book/%s' % book_id) rating = form.cleaned_data.get('rating') # throws a value error if the book is not found outgoing.handle_rate(request.user, book_id, rating) return redirect('/book/%s' % book_id) @login_required def review(request): ''' create a book review ''' form = forms.ReviewForm(request.POST) book_id = request.POST.get('book') if not form.is_valid(): return redirect('/book/%s' % book_id) # TODO: validation, htmlification name = form.cleaned_data.get('name') content = form.cleaned_data.get('content') rating = form.data.get('rating', None) try: rating = int(rating) except ValueError: rating = None outgoing.handle_review(request.user, book_id, name, content, rating) return redirect('/book/%s' % book_id) @login_required def quotate(request): ''' create a book quotation ''' form = forms.QuotationForm(request.POST) book_id = request.POST.get('book') if not form.is_valid(): return redirect('/book/%s' % book_id) quote = form.cleaned_data.get('quote') content = form.cleaned_data.get('content') outgoing.handle_quotation(request.user, book_id, content, quote) return redirect('/book/%s' % book_id) @login_required def comment(request): ''' create a book comment ''' form = forms.CommentForm(request.POST) book_id = request.POST.get('book') # TODO: better failure behavior if not form.is_valid(): return redirect('/book/%s' % book_id) # TODO: validation, htmlification content = form.data.get('content') outgoing.handle_comment(request.user, book_id, content) return redirect('/book/%s' % book_id) @login_required def tag(request): ''' tag a book ''' # I'm not using a form here because sometimes "name" is sent as a hidden # field which doesn't validate name = request.POST.get('name') book_id = request.POST.get('book') outgoing.handle_tag(request.user, book_id, name) return redirect('/book/%s' % book_id) @login_required def untag(request): ''' untag a book ''' name = request.POST.get('name') book_id = request.POST.get('book') outgoing.handle_untag(request.user, book_id, name) return redirect('/book/%s' % book_id) @login_required def reply(request): ''' respond to a book review ''' form = forms.ReplyForm(request.POST) # this is a bit of a formality, the form is just one text field if not form.is_valid(): return redirect('/') parent_id = request.POST['parent'] parent = models.Status.objects.get(id=parent_id) outgoing.handle_reply(request.user, parent, form.data['content']) return redirect('/') @login_required def favorite(request, status_id): ''' like a status ''' status = models.Status.objects.get(id=status_id) outgoing.handle_favorite(request.user, status) return redirect(request.headers.get('Referer', '/')) @login_required def unfavorite(request, status_id): ''' like a status ''' status = models.Status.objects.get(id=status_id) outgoing.handle_unfavorite(request.user, status) return redirect(request.headers.get('Referer', '/')) @login_required def boost(request, status_id): ''' boost a status ''' status = models.Status.objects.get(id=status_id) outgoing.handle_boost(request.user, status) return redirect(request.headers.get('Referer', '/')) @login_required def follow(request): ''' follow another user, here or abroad ''' username = request.POST['user'] try: to_follow = get_user_from_username(username) except models.User.DoesNotExist: return HttpResponseBadRequest() outgoing.handle_follow(request.user, to_follow) user_slug = to_follow.localname if to_follow.localname \ else to_follow.username return redirect('/user/%s' % user_slug) @login_required def unfollow(request): ''' unfollow a user ''' username = request.POST['user'] try: to_unfollow = get_user_from_username(username) except models.User.DoesNotExist: return HttpResponseBadRequest() outgoing.handle_unfollow(request.user, to_unfollow) user_slug = to_unfollow.localname if to_unfollow.localname \ else to_unfollow.username return redirect('/user/%s' % user_slug) @login_required def clear_notifications(request): ''' permanently delete notification for user ''' request.user.notification_set.filter(read=True).delete() return redirect('/notifications') @login_required def accept_follow_request(request): ''' a user accepts a follow request ''' username = request.POST['user'] try: requester = get_user_from_username(username) except models.User.DoesNotExist: return HttpResponseBadRequest() try: follow_request = models.UserFollowRequest.objects.get( user_subject=requester, user_object=request.user ) except models.UserFollowRequest.DoesNotExist: # Request already dealt with. pass else: outgoing.handle_accept(requester, request.user, follow_request) return redirect('/user/%s' % request.user.localname) @login_required def delete_follow_request(request): ''' a user rejects a follow request ''' username = request.POST['user'] try: requester = get_user_from_username(username) except models.User.DoesNotExist: return HttpResponseBadRequest() try: follow_request = models.UserFollowRequest.objects.get( user_subject=requester, user_object=request.user ) except models.UserFollowRequest.DoesNotExist: return HttpResponseBadRequest() outgoing.handle_reject(requester, request.user, follow_request) return redirect('/user/%s' % request.user.localname) @login_required def import_data(request): ''' ingest a goodreads csv ''' form = forms.ImportForm(request.POST, request.FILES) if form.is_valid(): try: job = goodreads_import.create_job( request.user, TextIOWrapper( request.FILES['csv_file'], encoding=request.encoding) ) except (UnicodeDecodeError, ValueError): return HttpResponseBadRequest('Not a valid csv file') goodreads_import.start_import(job) return redirect('/import_status/%d' % (job.id,)) return HttpResponseBadRequest()