forked from mirrors/bookwyrm
e2f39a1bd5
Allow users to set privacy on imported reviews
571 lines
17 KiB
Python
571 lines
17 KiB
Python
''' views for actions you can take in the application '''
|
|
from io import BytesIO, TextIOWrapper
|
|
from PIL import Image
|
|
|
|
import dateutil.parser
|
|
from dateutil.parser import ParserError
|
|
from django.contrib.auth import authenticate, login, logout
|
|
from django.contrib.auth.decorators import login_required, permission_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 django.core.exceptions import PermissionDenied
|
|
|
|
from bookwyrm import books_manager
|
|
from bookwyrm import forms, models, outgoing
|
|
from bookwyrm import goodreads_import
|
|
from bookwyrm.emailing import password_reset_email
|
|
from bookwyrm.settings import DOMAIN
|
|
from bookwyrm.views import get_user_from_username
|
|
|
|
|
|
def user_login(request):
|
|
''' authenticate user login '''
|
|
if request.method == 'GET':
|
|
return redirect('/login')
|
|
|
|
login_form = forms.LoginForm(request.POST)
|
|
|
|
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', '/'))
|
|
|
|
login_form.non_field_errors = 'Username or password are incorrect'
|
|
register_form = forms.RegisterForm()
|
|
data = {
|
|
'site_settings': models.SiteSettings.get(),
|
|
'login_form': login_form,
|
|
'register_form': register_form
|
|
}
|
|
return TemplateResponse(request, 'login.html', data)
|
|
|
|
|
|
def register(request):
|
|
''' join the server '''
|
|
if request.method == 'GET':
|
|
return redirect('/login')
|
|
|
|
if not models.SiteSettings.get().allow_registration:
|
|
invite_code = request.POST.get('invite_code')
|
|
|
|
if not invite_code:
|
|
raise PermissionDenied
|
|
|
|
try:
|
|
invite = models.SiteInvite.objects.get(code=invite_code)
|
|
except models.SiteInvite.DoesNotExist:
|
|
raise PermissionDenied
|
|
else:
|
|
invite = None
|
|
|
|
form = forms.RegisterForm(request.POST)
|
|
errors = False
|
|
if not form.is_valid():
|
|
errors = True
|
|
|
|
username = form.data['username']
|
|
email = form.data['email']
|
|
password = form.data['password']
|
|
|
|
# check username and email uniqueness
|
|
if models.User.objects.filter(localname=username).first():
|
|
form.add_error('username', 'User with this username already exists')
|
|
errors = True
|
|
|
|
if errors:
|
|
data = {
|
|
'site_settings': models.SiteSettings.get(),
|
|
'login_form': forms.LoginForm(),
|
|
'register_form': form
|
|
}
|
|
return TemplateResponse(request, 'login.html', data)
|
|
|
|
user = models.User.objects.create_user(username, email, password)
|
|
if invite:
|
|
invite.times_used += 1
|
|
invite.save()
|
|
|
|
login(request, user)
|
|
return redirect('/')
|
|
|
|
|
|
@login_required
|
|
def user_logout(request):
|
|
''' done with this place! outa here! '''
|
|
logout(request)
|
|
return redirect('/')
|
|
|
|
|
|
def password_reset_request(request):
|
|
''' create a password reset token '''
|
|
email = request.POST.get('email')
|
|
try:
|
|
user = models.User.objects.get(email=email)
|
|
except models.User.DoesNotExist:
|
|
return redirect('/password-reset')
|
|
|
|
# remove any existing password reset cods for this user
|
|
models.PasswordReset.objects.filter(user=user).all().delete()
|
|
|
|
# create a new reset code
|
|
code = models.PasswordReset.objects.create(user=user)
|
|
password_reset_email(code)
|
|
data = {'message': 'Password reset link sent to %s' % email}
|
|
return TemplateResponse(request, 'password_reset_request.html', data)
|
|
|
|
|
|
def password_reset(request):
|
|
''' allow a user to change their password through an emailed token '''
|
|
try:
|
|
reset_code = models.PasswordReset.objects.get(
|
|
code=request.POST.get('reset-code')
|
|
)
|
|
except models.PasswordReset.DoesNotExist:
|
|
data = {'errors': ['Invalid password reset link']}
|
|
return TemplateResponse(request, 'password_reset.html', data)
|
|
|
|
user = reset_code.user
|
|
|
|
new_password = request.POST.get('password')
|
|
confirm_password = request.POST.get('confirm-password')
|
|
|
|
if new_password != confirm_password:
|
|
data = {'errors': ['Passwords do not match']}
|
|
return TemplateResponse(request, 'password_reset.html', data)
|
|
|
|
user.set_password(new_password)
|
|
user.save()
|
|
login(request, user)
|
|
reset_code.delete()
|
|
return redirect('/')
|
|
|
|
|
|
@login_required
|
|
def password_change(request):
|
|
''' allow a user to change their password '''
|
|
new_password = request.POST.get('password')
|
|
confirm_password = request.POST.get('confirm-password')
|
|
|
|
if new_password != confirm_password:
|
|
return redirect('/user-edit')
|
|
|
|
request.user.set_password(new_password)
|
|
request.user.save()
|
|
login(request, request.user)
|
|
return redirect('/user-edit')
|
|
|
|
|
|
@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():
|
|
data = {
|
|
'form': form,
|
|
'user': request.user,
|
|
}
|
|
return TemplateResponse(request, 'edit_user.html', data)
|
|
|
|
request.user.name = form.data['name']
|
|
request.user.email = form.data['email']
|
|
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 = books_manager.get_or_create_book(remote_id)
|
|
return redirect('/book/%d' % book.id)
|
|
|
|
|
|
@login_required
|
|
@permission_required('bookwyrm.edit_book', raise_exception=True)
|
|
def edit_book(request, book_id):
|
|
''' edit a book cool '''
|
|
if not request.method == 'POST':
|
|
return redirect('/book/%s' % book_id)
|
|
|
|
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.Edition.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 edit_readthrough(request):
|
|
''' can't use the form because the dates are too finnicky '''
|
|
try:
|
|
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id'))
|
|
except models.ReadThrough.DoesNotExist:
|
|
return HttpResponseNotFound()
|
|
|
|
# don't let people edit other people's data
|
|
if request.user != readthrough.user:
|
|
return HttpResponseBadRequest()
|
|
|
|
# convert dates into a legible format
|
|
start_date = request.POST.get('start_date')
|
|
try:
|
|
start_date = dateutil.parser.parse(start_date)
|
|
except ParserError:
|
|
start_date = None
|
|
readthrough.start_date = start_date
|
|
finish_date = request.POST.get('finish_date')
|
|
try:
|
|
finish_date = dateutil.parser.parse(finish_date)
|
|
except ParserError:
|
|
finish_date = None
|
|
readthrough.finish_date = finish_date
|
|
readthrough.save()
|
|
return redirect(request.headers.get('Referer', '/'))
|
|
|
|
|
|
@login_required
|
|
def delete_readthrough(request):
|
|
''' remove a readthrough '''
|
|
try:
|
|
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id'))
|
|
except models.ReadThrough.DoesNotExist:
|
|
return HttpResponseNotFound()
|
|
|
|
# don't let people edit other people's data
|
|
if request.user != readthrough.user:
|
|
return HttpResponseBadRequest()
|
|
|
|
readthrough.delete()
|
|
return redirect(request.headers.get('Referer', '/'))
|
|
|
|
|
|
@login_required
|
|
def shelve(request):
|
|
''' put a on a user's shelf '''
|
|
book = books_manager.get_edition(request.POST['book'])
|
|
|
|
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 unshelve(request):
|
|
''' put a on a user's shelf '''
|
|
book = models.Edition.objects.get(id=request.POST['book'])
|
|
current_shelf = models.Shelf.objects.get(id=request.POST['shelf'])
|
|
|
|
outgoing.handle_unshelve(request.user, book, current_shelf)
|
|
return redirect(request.headers.get('Referer', '/'))
|
|
|
|
|
|
@login_required
|
|
def rate(request):
|
|
''' just a star rating for a book '''
|
|
form = forms.RatingForm(request.POST)
|
|
return handle_status(request, form)
|
|
|
|
|
|
@login_required
|
|
def review(request):
|
|
''' create a book review '''
|
|
form = forms.ReviewForm(request.POST)
|
|
return handle_status(request, form)
|
|
|
|
|
|
@login_required
|
|
def quotate(request):
|
|
''' create a book quotation '''
|
|
form = forms.QuotationForm(request.POST)
|
|
return handle_status(request, form)
|
|
|
|
|
|
@login_required
|
|
def comment(request):
|
|
''' create a book comment '''
|
|
form = forms.CommentForm(request.POST)
|
|
return handle_status(request, form)
|
|
|
|
|
|
@login_required
|
|
def reply(request):
|
|
''' respond to a book review '''
|
|
form = forms.ReplyForm(request.POST)
|
|
return handle_status(request, form)
|
|
|
|
|
|
def handle_status(request, form):
|
|
''' all the "create a status" functions are the same '''
|
|
if not form.is_valid():
|
|
return redirect(request.headers.get('Referer', '/'))
|
|
|
|
outgoing.handle_status(request.user, form)
|
|
return redirect(request.headers.get('Referer', '/'))
|
|
|
|
|
|
@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')
|
|
remote_id = 'https://%s/book/%s' % (DOMAIN, book_id)
|
|
|
|
outgoing.handle_tag(request.user, remote_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 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 delete_status(request):
|
|
''' delete and tombstone a status '''
|
|
status_id = request.POST.get('status')
|
|
if not status_id:
|
|
return HttpResponseBadRequest()
|
|
try:
|
|
status = models.Status.objects.get(id=status_id)
|
|
except models.Status.DoesNotExist:
|
|
return HttpResponseBadRequest()
|
|
|
|
# don't let people delete other people's statuses
|
|
if status.user != request.user:
|
|
return HttpResponseBadRequest()
|
|
|
|
# perform deletion
|
|
outgoing.handle_delete_status(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(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(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():
|
|
include_reviews = request.POST.get('include_reviews') == 'on'
|
|
privacy = request.POST.get('privacy')
|
|
try:
|
|
job = goodreads_import.create_job(
|
|
request.user,
|
|
TextIOWrapper(
|
|
request.FILES['csv_file'],
|
|
encoding=request.encoding),
|
|
include_reviews,
|
|
privacy,
|
|
)
|
|
except (UnicodeDecodeError, ValueError):
|
|
return HttpResponseBadRequest('Not a valid csv file')
|
|
goodreads_import.start_import(job)
|
|
return redirect('/import_status/%d' % (job.id,))
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
@permission_required('bookwyrm.create_invites', raise_exception=True)
|
|
def create_invite(request):
|
|
''' creates a user invite database entry '''
|
|
form = forms.CreateInviteForm(request.POST)
|
|
if not form.is_valid():
|
|
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
|
|
|
|
invite = form.save(commit=False)
|
|
invite.user = request.user
|
|
invite.save()
|
|
|
|
return redirect('/invite')
|