Merge branch 'main' into create-book

This commit is contained in:
Mouse Reeve
2021-03-08 10:07:02 -08:00
committed by GitHub
201 changed files with 10253 additions and 8555 deletions

View File

@ -1,4 +1,4 @@
''' make sure all our nice views are available '''
""" make sure all our nice views are available """
from .authentication import Login, Register, Logout
from .author import Author, EditAuthor
from .block import Block, unblock

View File

@ -1,4 +1,4 @@
''' class views for login/register views '''
""" class views for login/register views """
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
@ -14,60 +14,59 @@ from bookwyrm.settings import DOMAIN
# pylint: disable= no-self-use
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(csrf_exempt, name="dispatch")
class Login(View):
''' authenticate an existing user '''
""" authenticate an existing user """
def get(self, request):
''' login page '''
""" login page """
if request.user.is_authenticated:
return redirect('/')
return redirect("/")
# sene user to the login page
data = {
'login_form': forms.LoginForm(),
'register_form': forms.RegisterForm(),
"login_form": forms.LoginForm(),
"register_form": forms.RegisterForm(),
}
return TemplateResponse(request, 'login.html', data)
return TemplateResponse(request, "login.html", data)
def post(self, request):
''' authentication action '''
""" authentication action """
if request.user.is_authenticated:
return redirect('/')
return redirect("/")
login_form = forms.LoginForm(request.POST)
localname = login_form.data['localname']
if '@' in localname: # looks like an email address to me
localname = login_form.data["localname"]
if "@" in localname: # looks like an email address to me
email = localname
try:
username = models.User.objects.get(email=email)
except models.User.DoesNotExist: # maybe it's a full username?
except models.User.DoesNotExist: # maybe it's a full username?
username = localname
else:
username = '%s@%s' % (localname, DOMAIN)
password = login_form.data['password']
username = "%s@%s" % (localname, DOMAIN)
password = login_form.data["password"]
user = authenticate(request, username=username, password=password)
if user is not None:
# successful login
login(request, user)
user.last_active_date = timezone.now()
user.save(broadcast=False)
return redirect(request.GET.get('next', '/'))
return redirect(request.GET.get("next", "/"))
# login errors
login_form.non_field_errors = 'Username or password are incorrect'
login_form.non_field_errors = "Username or password are incorrect"
register_form = forms.RegisterForm()
data = {
'login_form': login_form,
'register_form': register_form
}
return TemplateResponse(request, 'login.html', data)
data = {"login_form": login_form, "register_form": register_form}
return TemplateResponse(request, "login.html", data)
class Register(View):
''' register a user '''
""" register a user """
def post(self, request):
''' join the server '''
""" join the server """
if not models.SiteSettings.get().allow_registration:
invite_code = request.POST.get('invite_code')
invite_code = request.POST.get("invite_code")
if not invite_code:
raise PermissionDenied
@ -83,42 +82,43 @@ class Register(View):
if not form.is_valid():
errors = True
localname = form.data['localname'].strip()
email = form.data['email']
password = form.data['password']
localname = form.data["localname"].strip()
email = form.data["email"]
password = form.data["password"]
# check localname and email uniqueness
if models.User.objects.filter(localname=localname).first():
form.errors['localname'] = [
'User with this username already exists']
form.errors["localname"] = ["User with this username already exists"]
errors = True
if errors:
data = {
'login_form': forms.LoginForm(),
'register_form': form,
'invite': invite,
'valid': invite.valid() if invite else True,
"login_form": forms.LoginForm(),
"register_form": form,
"invite": invite,
"valid": invite.valid() if invite else True,
}
if invite:
return TemplateResponse(request, 'invite.html', data)
return TemplateResponse(request, 'login.html', data)
return TemplateResponse(request, "invite.html", data)
return TemplateResponse(request, "login.html", data)
username = '%s@%s' % (localname, DOMAIN)
username = "%s@%s" % (localname, DOMAIN)
user = models.User.objects.create_user(
username, email, password, localname=localname, local=True)
username, email, password, localname=localname, local=True
)
if invite:
invite.times_used += 1
invite.save()
login(request, user)
return redirect('/')
return redirect("/")
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Logout(View):
''' log out '''
""" log out """
def get(self, request):
''' done with this place! outa here! '''
""" done with this place! outa here! """
logout(request)
return redirect('/')
return redirect("/")

View File

@ -1,4 +1,4 @@
''' the good people stuff! the authors! '''
""" the good people stuff! the authors! """
from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect
@ -13,49 +13,46 @@ from .helpers import is_api_request
# pylint: disable= no-self-use
class Author(View):
''' this person wrote a book '''
""" this person wrote a book """
def get(self, request, author_id):
''' landing page for an author '''
""" landing page for an author """
author = get_object_or_404(models.Author, id=author_id)
if is_api_request(request):
return ActivitypubResponse(author.to_activity())
books = models.Work.objects.filter(
Q(authors=author) | Q(editions__authors=author)).distinct()
Q(authors=author) | Q(editions__authors=author)
).distinct()
data = {
'author': author,
'books': [b.get_default_edition() for b in books],
"author": author,
"books": [b.get_default_edition() for b in books],
}
return TemplateResponse(request, 'author.html', data)
return TemplateResponse(request, "author.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required('bookwyrm.edit_book', raise_exception=True),
name='dispatch')
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
)
class EditAuthor(View):
''' edit author info '''
""" edit author info """
def get(self, request, author_id):
''' info about a book '''
""" info about a book """
author = get_object_or_404(models.Author, id=author_id)
data = {
'author': author,
'form': forms.AuthorForm(instance=author)
}
return TemplateResponse(request, 'edit_author.html', data)
data = {"author": author, "form": forms.AuthorForm(instance=author)}
return TemplateResponse(request, "edit_author.html", data)
def post(self, request, author_id):
''' edit a author cool '''
""" edit a author cool """
author = get_object_or_404(models.Author, id=author_id)
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
if not form.is_valid():
data = {
'author': author,
'form': form
}
return TemplateResponse(request, 'edit_author.html', data)
data = {"author": author, "form": form}
return TemplateResponse(request, "edit_author.html", data)
author = form.save()
return redirect('/author/%s' % author.id)
return redirect("/author/%s" % author.id)

View File

@ -1,4 +1,4 @@
''' views for actions you can take in the application '''
""" views for actions you can take in the application """
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
@ -10,25 +10,27 @@ from django.views.decorators.http import require_POST
from bookwyrm import models
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Block(View):
''' blocking users '''
""" blocking users """
def get(self, request):
''' list of blocked users? '''
return TemplateResponse(request, 'preferences/blocks.html')
""" list of blocked users? """
return TemplateResponse(request, "preferences/blocks.html")
def post(self, request, user_id):
''' block a user '''
""" block a user """
to_block = get_object_or_404(models.User, id=user_id)
models.UserBlocks.objects.create(
user_subject=request.user, user_object=to_block)
return redirect('/preferences/block')
user_subject=request.user, user_object=to_block
)
return redirect("/preferences/block")
@require_POST
@login_required
def unblock(request, user_id):
''' undo a block '''
""" undo a block """
to_unblock = get_object_or_404(models.User, id=user_id)
try:
block = models.UserBlocks.objects.get(
@ -38,4 +40,4 @@ def unblock(request, user_id):
except models.UserBlocks.DoesNotExist:
return HttpResponseNotFound()
block.delete()
return redirect('/preferences/block')
return redirect("/preferences/block")

View File

@ -1,4 +1,5 @@
''' the good stuff! the books! '''
""" the good stuff! the books! """
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector
from django.core.paginator import Paginator
@ -21,11 +22,12 @@ from .helpers import privacy_filter
# pylint: disable= no-self-use
class Book(View):
''' a book! this is the stuff '''
""" a book! this is the stuff """
def get(self, request, book_id):
''' info about a book '''
""" info about a book """
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
@ -51,30 +53,28 @@ class Book(View):
reviews = get_activity_feed(request.user, queryset=reviews)
# the reviews to show
paginated = Paginator(reviews.exclude(
Q(content__isnull=True) | Q(content='')
), PAGE_LENGTH)
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)
).values_list("tag__identifier", flat=True)
readthroughs = models.ReadThrough.objects.filter(
user=request.user,
book=book,
).order_by('start_date')
).order_by("start_date")
for readthrough in readthroughs:
readthrough.progress_updates = \
readthrough.progressupdate_set.all() \
.order_by('-updated_date')
readthrough.progress_updates = (
readthrough.progressupdate_set.all().order_by("-updated_date")
)
user_shelves = models.ShelfBook.objects.filter(
user=request.user, book=book
)
user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book)
other_edition_shelves = models.ShelfBook.objects.filter(
~Q(book=book),
@ -83,28 +83,28 @@ class Book(View):
)
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(
"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,
"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)
return TemplateResponse(request, "book.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required('bookwyrm.edit_book', raise_exception=True),
name='dispatch')
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
)
class EditBook(View):
''' edit a book '''
def get(self, request, book_id=None):
@ -218,81 +218,81 @@ class ConfirmEditBook(View):
for author_id in request.POST.getlist('remove_authors'):
book.authors.remove(author_id)
return redirect('/book/%s' % book.id)
return redirect("/book/%s" % book.id)
class Editions(View):
''' list of editions '''
""" list of editions """
def get(self, request, book_id):
''' list of editions of a book '''
""" 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,
"editions": work.editions.order_by("-edition_rank").all(),
"work": work,
}
return TemplateResponse(request, 'editions.html', data)
return TemplateResponse(request, "editions.html", data)
@login_required
@require_POST
def upload_cover(request, book_id):
''' upload a new cover '''
""" 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)
return redirect("/book/%d" % book.id)
book.last_edited_by = request.user
book.cover = form.files['cover']
book.cover = form.files["cover"]
book.save()
return redirect('/book/%s' % book.id)
return redirect("/book/%s" % book.id)
@login_required
@require_POST
@permission_required('bookwyrm.edit_book', raise_exception=True)
@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('/')
""" 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')
description = request.POST.get("description")
book.description = description
book.last_edited_by = request.user
book.save()
return redirect('/book/%s' % book.id)
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')
""" 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)
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')
""" 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
book__parent_work=new_edition.parent_work, shelf__user=request.user
)
for shelfbook in shelfbooks.all():
with transaction.atomic():
@ -300,16 +300,15 @@ def switch_edition(request):
created_date=shelfbook.created_date,
user=shelfbook.user,
shelf=shelfbook.shelf,
book=new_edition
book=new_edition,
)
shelfbook.delete()
readthroughs = models.ReadThrough.objects.filter(
book__parent_work=new_edition.parent_work,
user=request.user
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)
return redirect("/book/%d" % new_edition.id)

View File

@ -1,11 +1,12 @@
''' something has gone amiss '''
""" something has gone amiss """
from django.template.response import TemplateResponse
def server_error_page(request):
''' 500 errors '''
return TemplateResponse(request, 'error.html', status=500)
""" 500 errors """
return TemplateResponse(request, "error.html", status=500)
def not_found_page(request, _):
''' 404s '''
return TemplateResponse(request, 'notfound.html', status=404)
""" 404s """
return TemplateResponse(request, "notfound.html", status=404)

View File

@ -1,4 +1,4 @@
''' manage federated servers '''
""" manage federated servers """
from django.contrib.auth.decorators import login_required, permission_required
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
@ -8,14 +8,16 @@ from bookwyrm import models
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required('bookwyrm.control_federation', raise_exception=True),
name='dispatch')
permission_required("bookwyrm.control_federation", raise_exception=True),
name="dispatch",
)
class Federation(View):
''' what servers do we federate with '''
""" what servers do we federate with """
def get(self, request):
''' edit form '''
""" edit form """
servers = models.FederatedServer.objects.all()
data = {'servers': servers}
return TemplateResponse(request, 'settings/federation.html', data)
data = {"servers": servers}
return TemplateResponse(request, "settings/federation.html", data)

View File

@ -1,4 +1,4 @@
''' non-interactive pages '''
""" non-interactive pages """
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
@ -17,48 +17,54 @@ from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Feed(View):
''' activity stream '''
""" activity stream """
def get(self, request, tab):
''' user's homepage with activity feed '''
""" user's homepage with activity feed """
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
if tab == 'home':
if tab == "home":
activities = get_activity_feed(request.user, following_only=True)
tab_title = _("Home")
elif tab == "local":
activities = get_activity_feed(
request.user, following_only=True)
tab_title = _('Home')
elif tab == 'local':
activities = get_activity_feed(
request.user, privacy=['public', 'followers'], local_only=True)
tab_title = _('Local')
request.user, privacy=["public", "followers"], local_only=True
)
tab_title = _("Local")
else:
activities = get_activity_feed(
request.user, privacy=['public', 'followers'])
tab_title = _('Federated')
request.user, privacy=["public", "followers"]
)
tab_title = _("Federated")
paginated = Paginator(activities, PAGE_LENGTH)
data = {**feed_page_data(request.user), **{
'user': request.user,
'activities': paginated.page(page),
'tab': tab,
'tab_title': tab_title,
'goal_form': forms.GoalForm(),
'path': '/%s' % tab,
}}
return TemplateResponse(request, 'feed/feed.html', data)
data = {
**feed_page_data(request.user),
**{
"user": request.user,
"activities": paginated.page(page),
"tab": tab,
"tab_title": tab_title,
"goal_form": forms.GoalForm(),
"path": "/%s" % tab,
},
}
return TemplateResponse(request, "feed/feed.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class DirectMessage(View):
''' dm view '''
""" dm view """
def get(self, request, username=None):
''' like a feed but for dms only '''
""" like a feed but for dms only """
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
@ -74,27 +80,33 @@ class DirectMessage(View):
queryset = queryset.filter(Q(user=user) | Q(mention_users=user))
activities = get_activity_feed(
request.user, privacy=['direct'], queryset=queryset)
request.user, privacy=["direct"], queryset=queryset
)
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
data = {**feed_page_data(request.user), **{
'user': request.user,
'partner': user,
'activities': activity_page,
'path': '/direct-messages',
}}
return TemplateResponse(request, 'feed/direct_messages.html', data)
data = {
**feed_page_data(request.user),
**{
"user": request.user,
"partner": user,
"activities": activity_page,
"path": "/direct-messages",
},
}
return TemplateResponse(request, "feed/direct_messages.html", data)
class Status(View):
''' get posting '''
""" get posting """
def get(self, request, username, status_id):
''' display a particular status (and replies, etc) '''
""" display a particular status (and replies, etc) """
try:
user = get_user_from_username(request.user, username)
status = models.Status.objects.select_subclasses().get(
id=status_id, deleted=False)
id=status_id, deleted=False
)
except ValueError:
return HttpResponseNotFound()
@ -108,18 +120,23 @@ class Status(View):
if is_api_request(request):
return ActivitypubResponse(
status.to_activity(pure=not is_bookwyrm_request(request)))
status.to_activity(pure=not is_bookwyrm_request(request))
)
data = {**feed_page_data(request.user), **{
'status': status,
}}
return TemplateResponse(request, 'feed/status.html', data)
data = {
**feed_page_data(request.user),
**{
"status": status,
},
}
return TemplateResponse(request, "feed/status.html", data)
class Replies(View):
''' replies page (a json view of status) '''
""" replies page (a json view of status) """
def get(self, request, username, status_id):
''' ordered collection of replies to a status '''
""" ordered collection of replies to a status """
# the html view is the same as Status
if not is_api_request(request):
status_view = Status.as_view()
@ -134,41 +151,39 @@ class Replies(View):
def feed_page_data(user):
''' info we need for every feed page '''
""" info we need for every feed page """
if not user.is_authenticated:
return {}
goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year
).first()
goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first()
return {
'suggested_books': get_suggested_books(user),
'goal': goal,
'goal_form': forms.GoalForm(),
"suggested_books": get_suggested_books(user),
"goal": goal,
"goal_form": forms.GoalForm(),
}
def get_suggested_books(user, max_books=5):
''' helper to get a user's recent books '''
""" helper to get a user's recent books """
book_count = 0
preset_shelves = [
('reading', max_books), ('read', 2), ('to-read', max_books)
]
preset_shelves = [("reading", max_books), ("read", 2), ("to-read", max_books)]
suggested_books = []
for (preset, shelf_max) in preset_shelves:
limit = shelf_max if shelf_max < (max_books - book_count) \
else max_books - book_count
limit = (
shelf_max
if shelf_max < (max_books - book_count)
else max_books - book_count
)
shelf = user.shelf_set.get(identifier=preset)
shelf_books = shelf.shelfbook_set.order_by(
'-updated_date'
).all()[:limit]
shelf_books = shelf.shelfbook_set.order_by("-updated_date").all()[:limit]
if not shelf_books:
continue
shelf_preview = {
'name': shelf.name,
'identifier': shelf.identifier,
'books': [s.book for s in shelf_books]
"name": shelf.name,
"identifier": shelf.identifier,
"books": [s.book for s in shelf_books],
}
suggested_books.append(shelf_preview)
book_count += len(shelf_preview['books'])
book_count += len(shelf_preview["books"])
return suggested_books

View File

@ -1,4 +1,4 @@
''' views for actions you can take in the application '''
""" views for actions you can take in the application """
from django.contrib.auth.decorators import login_required
from django.db import IntegrityError
from django.http import HttpResponseBadRequest
@ -8,11 +8,12 @@ from django.views.decorators.http import require_POST
from bookwyrm import models
from .helpers import get_user_from_username
@login_required
@require_POST
def follow(request):
''' follow another user, here or abroad '''
username = request.POST['user']
""" follow another user, here or abroad """
username = request.POST["user"]
try:
to_follow = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -32,16 +33,15 @@ def follow(request):
@login_required
@require_POST
def unfollow(request):
''' unfollow a user '''
username = request.POST['user']
""" unfollow a user """
username = request.POST["user"]
try:
to_unfollow = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
models.UserFollows.objects.get(
user_subject=request.user,
user_object=to_unfollow
user_subject=request.user, user_object=to_unfollow
).delete()
return redirect(to_unfollow.local_path)
@ -49,8 +49,8 @@ def unfollow(request):
@login_required
@require_POST
def accept_follow_request(request):
''' a user accepts a follow request '''
username = request.POST['user']
""" a user accepts a follow request """
username = request.POST["user"]
try:
requester = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -58,8 +58,7 @@ def accept_follow_request(request):
try:
follow_request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=request.user
user_subject=requester, user_object=request.user
)
except models.UserFollowRequest.DoesNotExist:
# Request already dealt with.
@ -72,8 +71,8 @@ def accept_follow_request(request):
@login_required
@require_POST
def delete_follow_request(request):
''' a user rejects a follow request '''
username = request.POST['user']
""" a user rejects a follow request """
username = request.POST["user"]
try:
requester = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -81,11 +80,10 @@ def delete_follow_request(request):
try:
follow_request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=request.user
user_subject=requester, user_object=request.user
)
except models.UserFollowRequest.DoesNotExist:
return HttpResponseBadRequest()
follow_request.delete()
return redirect('/user/%s' % request.user.localname)
return redirect("/user/%s" % request.user.localname)

View File

@ -1,4 +1,4 @@
''' non-interactive pages '''
""" non-interactive pages """
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
@ -13,16 +13,15 @@ from .helpers import get_user_from_username, object_visible_to_user
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Goal(View):
''' track books for the year '''
""" track books for the year """
def get(self, request, username, year):
''' reading goal page '''
""" reading goal page """
user = get_user_from_username(request.user, username)
year = int(year)
goal = models.AnnualGoal.objects.filter(
year=year, user=user
).first()
goal = models.AnnualGoal.objects.filter(year=year, user=user).first()
if not goal and user != request.user:
return HttpResponseNotFound()
@ -30,42 +29,39 @@ class Goal(View):
return HttpResponseNotFound()
data = {
'goal_form': forms.GoalForm(instance=goal),
'goal': goal,
'user': user,
'year': year,
'is_self': request.user == user,
"goal_form": forms.GoalForm(instance=goal),
"goal": goal,
"user": user,
"year": year,
"is_self": request.user == user,
}
return TemplateResponse(request, 'goal.html', data)
return TemplateResponse(request, "goal.html", data)
def post(self, request, username, year):
''' update or create an annual goal '''
""" update or create an annual goal """
user = get_user_from_username(request.user, username)
if user != request.user:
return HttpResponseNotFound()
year = int(year)
goal = models.AnnualGoal.objects.filter(
year=year, user=request.user
).first()
goal = models.AnnualGoal.objects.filter(year=year, user=request.user).first()
form = forms.GoalForm(request.POST, instance=goal)
if not form.is_valid():
data = {
'goal_form': form,
'goal': goal,
'year': year,
"goal_form": form,
"goal": goal,
"year": year,
}
return TemplateResponse(request, 'goal.html', data)
return TemplateResponse(request, "goal.html", data)
goal = form.save()
if request.POST.get('post-status'):
if request.POST.get("post-status"):
# create status, if appropraite
template = get_template('snippets/generated_status/goal.html')
template = get_template("snippets/generated_status/goal.html")
create_generated_note(
request.user,
template.render({'goal': goal, 'user': request.user}).strip(),
privacy=goal.privacy
template.render({"goal": goal, "user": request.user}).strip(),
privacy=goal.privacy,
)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))

View File

@ -1,4 +1,4 @@
''' helper functions used in various views '''
""" helper functions used in various views """
import re
from requests import HTTPError
from django.core.exceptions import FieldError
@ -11,7 +11,7 @@ from bookwyrm.utils import regex
def get_user_from_username(viewer, username):
''' helper function to resolve a localname or a username to a user '''
""" helper function to resolve a localname or a username to a user """
# raises DoesNotExist if user is now found
try:
return models.User.viewer_aware_objects(viewer).get(localname=username)
@ -20,22 +20,20 @@ def get_user_from_username(viewer, username):
def is_api_request(request):
''' check whether a request is asking for html or data '''
return 'json' in request.headers.get('Accept') or \
request.path[-5:] == '.json'
""" check whether a request is asking for html or data """
return "json" in request.headers.get("Accept") or request.path[-5:] == ".json"
def is_bookwyrm_request(request):
''' check if the request is coming from another bookwyrm instance '''
user_agent = request.headers.get('User-Agent')
if user_agent is None or \
re.search(regex.bookwyrm_user_agent, user_agent) is None:
""" check if the request is coming from another bookwyrm instance """
user_agent = request.headers.get("User-Agent")
if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None:
return False
return True
def object_visible_to_user(viewer, obj):
''' is a user authorized to view an object? '''
""" is a user authorized to view an object? """
if not obj:
return False
@ -44,37 +42,32 @@ def object_visible_to_user(viewer, obj):
return False
# you can see your own posts and any public or unlisted posts
if viewer == obj.user or obj.privacy in ['public', 'unlisted']:
if viewer == obj.user or obj.privacy in ["public", "unlisted"]:
return True
# you can see the followers only posts of people you follow
if obj.privacy == 'followers' and \
obj.user.followers.filter(id=viewer.id).first():
if obj.privacy == "followers" and obj.user.followers.filter(id=viewer.id).first():
return True
# you can see dms you are tagged in
if isinstance(obj, models.Status):
if obj.privacy == 'direct' and \
obj.mention_users.filter(id=viewer.id).first():
if obj.privacy == "direct" and obj.mention_users.filter(id=viewer.id).first():
return True
return False
def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
''' filter objects that have "user" and "privacy" fields '''
privacy_levels = privacy_levels or \
['public', 'unlisted', 'followers', 'direct']
""" filter objects that have "user" and "privacy" fields """
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
# exclude blocks from both directions
if not viewer.is_anonymous:
blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()
queryset = queryset.exclude(
Q(user__in=blocked) | Q(user__blocks=viewer))
queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))
# you can't see followers only or direct messages if you're not logged in
if viewer.is_anonymous:
privacy_levels = [p for p in privacy_levels if \
not p in ['followers', 'direct']]
privacy_levels = [p for p in privacy_levels if not p in ["followers", "direct"]]
# filter to only privided privacy levels
queryset = queryset.filter(privacy__in=privacy_levels)
@ -82,53 +75,48 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
# only include statuses the user follows
if following_only:
queryset = queryset.exclude(
~Q(# remove everythign except
Q(user__in=viewer.following.all()) | # user following
Q(user=viewer) |# is self
Q(mention_users=viewer)# mentions user
~Q( # remove everythign except
Q(user__in=viewer.following.all())
| Q(user=viewer) # user following
| Q(mention_users=viewer) # is self # mentions user
),
)
# exclude followers-only statuses the user doesn't follow
elif 'followers' in privacy_levels:
elif "followers" in privacy_levels:
queryset = queryset.exclude(
~Q(# user isn't following and it isn't their own status
~Q( # user isn't following and it isn't their own status
Q(user__in=viewer.following.all()) | Q(user=viewer)
),
privacy='followers' # and the status is followers only
privacy="followers", # and the status is followers only
)
# exclude direct messages not intended for the user
if 'direct' in privacy_levels:
if "direct" in privacy_levels:
try:
queryset = queryset.exclude(
~Q(
Q(user=viewer) | Q(mention_users=viewer)
), privacy='direct'
~Q(Q(user=viewer) | Q(mention_users=viewer)), privacy="direct"
)
except FieldError:
queryset = queryset.exclude(
~Q(user=viewer), privacy='direct'
)
queryset = queryset.exclude(~Q(user=viewer), privacy="direct")
return queryset
def get_activity_feed(
user, privacy=None, local_only=False, following_only=False,
queryset=None):
''' get a filtered queryset of statuses '''
user, privacy=None, local_only=False, following_only=False, queryset=None
):
""" get a filtered queryset of statuses """
if queryset is None:
queryset = models.Status.objects.select_subclasses()
# exclude deleted
queryset = queryset.exclude(deleted=True).order_by('-published_date')
queryset = queryset.exclude(deleted=True).order_by("-published_date")
# apply privacy filters
queryset = privacy_filter(
user, queryset, privacy, following_only=following_only)
queryset = privacy_filter(user, queryset, privacy, following_only=following_only)
# only show dms if we only want dms
if privacy == ['direct']:
if privacy == ["direct"]:
# dms are direct statuses not related to books
queryset = queryset.filter(
review__isnull=True,
@ -143,7 +131,7 @@ def get_activity_feed(
comment__isnull=True,
quotation__isnull=True,
generatednote__isnull=True,
privacy='direct'
privacy="direct",
)
except FieldError:
# if we're looking at a subtype of Status (like Review)
@ -163,36 +151,35 @@ def get_activity_feed(
def handle_remote_webfinger(query):
''' webfingerin' other servers '''
""" webfingerin' other servers """
user = None
# usernames could be @user@domain or user@domain
if not query:
return None
if query[0] == '@':
if query[0] == "@":
query = query[1:]
try:
domain = query.split('@')[1]
domain = query.split("@")[1]
except IndexError:
return None
try:
user = models.User.objects.get(username=query)
except models.User.DoesNotExist:
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
(domain, query)
url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query)
try:
data = get_data(url)
except (ConnectorException, HTTPError):
return None
for link in data.get('links'):
if link.get('rel') == 'self':
for link in data.get("links"):
if link.get("rel") == "self":
try:
user = activitypub.resolve_remote_id(
link['href'], model=models.User
link["href"], model=models.User
)
except KeyError:
return None
@ -200,7 +187,7 @@ def handle_remote_webfinger(query):
def get_edition(book_id):
''' look up a book in the db and return an edition '''
""" look up a book in the db and return an edition """
book = models.Book.objects.select_subclasses().get(id=book_id)
if isinstance(book, models.Work):
book = book.get_default_edition()
@ -208,29 +195,24 @@ def get_edition(book_id):
def handle_reading_status(user, shelf, book, privacy):
''' post about a user reading a book '''
""" post about a user reading a book """
# tell the world about this cool thing that happened
try:
message = {
'to-read': 'wants to read',
'reading': 'started reading',
'read': 'finished reading'
"to-read": "wants to read",
"reading": "started reading",
"read": "finished reading",
}[shelf.identifier]
except KeyError:
# it's a non-standard shelf, don't worry about it
return
status = create_generated_note(
user,
message,
mention_books=[book],
privacy=privacy
)
status = create_generated_note(user, message, mention_books=[book], privacy=privacy)
status.save()
def is_blocked(viewer, user):
''' is this viewer blocked by the user? '''
""" is this viewer blocked by the user? """
if viewer.is_authenticated and viewer in user.blocks.all():
return True
return False

View File

@ -1,4 +1,4 @@
''' import books from another app '''
""" import books from another app """
from io import TextIOWrapper
from django.contrib.auth.decorators import login_required
@ -13,27 +13,33 @@ from bookwyrm import forms, goodreads_import, librarything_import, models
from bookwyrm.tasks import app
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Import(View):
''' import view '''
""" import view """
def get(self, request):
''' load import page '''
return TemplateResponse(request, 'import.html', {
'import_form': forms.ImportForm(),
'jobs': models.ImportJob.
objects.filter(user=request.user).order_by('-created_date'),
})
""" load import page """
return TemplateResponse(
request,
"import.html",
{
"import_form": forms.ImportForm(),
"jobs": models.ImportJob.objects.filter(user=request.user).order_by(
"-created_date"
),
},
)
def post(self, request):
''' ingest a goodreads csv '''
""" 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')
source = request.POST.get('source')
include_reviews = request.POST.get("include_reviews") == "on"
privacy = request.POST.get("privacy")
source = request.POST.get("source")
importer = None
if source == 'LibraryThing':
if source == "LibraryThing":
importer = librarything_import.LibrarythingImporter()
else:
# Default : GoodReads
@ -43,44 +49,44 @@ class Import(View):
job = importer.create_job(
request.user,
TextIOWrapper(
request.FILES['csv_file'],
encoding=importer.encoding),
request.FILES["csv_file"], encoding=importer.encoding
),
include_reviews,
privacy,
)
except (UnicodeDecodeError, ValueError):
return HttpResponseBadRequest('Not a valid csv file')
return HttpResponseBadRequest("Not a valid csv file")
importer.start_import(job)
return redirect('/import/%d' % job.id)
return redirect("/import/%d" % job.id)
return HttpResponseBadRequest()
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class ImportStatus(View):
''' status of an existing import '''
""" status of an existing import """
def get(self, request, job_id):
''' status of an import job '''
""" status of an import job """
job = models.ImportJob.objects.get(id=job_id)
if job.user != request.user:
raise PermissionDenied
task = app.AsyncResult(job.task_id)
items = job.items.order_by('index').all()
items = job.items.order_by("index").all()
failed_items = [i for i in items if i.fail_reason]
items = [i for i in items if not i.fail_reason]
return TemplateResponse(request, 'import_status.html', {
'job': job,
'items': items,
'failed_items': failed_items,
'task': task
})
return TemplateResponse(
request,
"import_status.html",
{"job": job, "items": items, "failed_items": failed_items, "task": task},
)
def post(self, request, job_id):
''' retry lines from an import '''
""" retry lines from an import """
job = get_object_or_404(models.ImportJob, id=job_id)
items = []
for item in request.POST.getlist('import_item'):
for item in request.POST.getlist("import_item"):
items.append(get_object_or_404(models.ImportItem, id=item))
job = goodreads_import.create_retry_job(
@ -89,4 +95,4 @@ class ImportStatus(View):
items,
)
goodreads_import.start_import(job)
return redirect('/import/%d' % job.id)
return redirect("/import/%d" % job.id)

View File

@ -1,4 +1,4 @@
''' incoming activities '''
""" incoming activities """
import json
from urllib.parse import urldefrag
@ -14,12 +14,13 @@ from bookwyrm.tasks import app
from bookwyrm.signatures import Signature
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(csrf_exempt, name="dispatch")
# pylint: disable=no-self-use
class Inbox(View):
''' requests sent by outside servers'''
""" requests sent by outside servers"""
def post(self, request, username=None):
''' only works as POST request '''
""" only works as POST request """
# make sure the user's inbox even exists
if username:
try:
@ -33,14 +34,16 @@ class Inbox(View):
except json.decoder.JSONDecodeError:
return HttpResponseBadRequest()
if not 'object' in activity_json or \
not 'type' in activity_json or \
not activity_json['type'] in activitypub.activity_objects:
if (
not "object" in activity_json
or not "type" in activity_json
or not activity_json["type"] in activitypub.activity_objects
):
return HttpResponseNotFound()
# verify the signature
if not has_valid_signature(request, activity_json):
if activity_json['type'] == 'Delete':
if activity_json["type"] == "Delete":
# Pretend that unauth'd deletes succeed. Auth may be failing
# because the resource or owner of the resource might have
# been deleted.
@ -53,7 +56,7 @@ class Inbox(View):
@app.task
def activity_task(activity_json):
''' do something with this json we think is legit '''
""" do something with this json we think is legit """
# lets see if the activitypub module can make sense of this json
try:
activity = activitypub.parse(activity_json)
@ -70,16 +73,15 @@ def activity_task(activity_json):
def has_valid_signature(request, activity):
''' verify incoming signature '''
""" verify incoming signature """
try:
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get('actor'):
if key_actor != activity.get("actor"):
raise ValueError("Wrong actor created signature.")
remote_user = activitypub.resolve_remote_id(
key_actor, model=models.User)
remote_user = activitypub.resolve_remote_id(key_actor, model=models.User)
if not remote_user:
return False
@ -91,7 +93,7 @@ def has_valid_signature(request, activity):
remote_user.remote_id, model=models.User, refresh=True
)
if remote_user.key_pair.public_key == old_key:
raise # Key unchanged.
raise # Key unchanged.
signature.verify(remote_user.key_pair.public_key, request)
except (ValueError, requests.exceptions.HTTPError):
return False

View File

@ -1,4 +1,4 @@
''' boosts and favs '''
""" boosts and favs """
from django.db import IntegrityError
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponseNotFound
@ -10,75 +10,74 @@ from bookwyrm import models
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Favorite(View):
''' like a status '''
""" like a status """
def post(self, request, status_id):
''' create a like '''
""" create a like """
status = models.Status.objects.get(id=status_id)
try:
models.Favorite.objects.create(
status=status,
user=request.user
)
models.Favorite.objects.create(status=status, user=request.user)
except IntegrityError:
# you already fav'ed that
return HttpResponseBadRequest()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Unfavorite(View):
''' take back a fav '''
""" take back a fav """
def post(self, request, status_id):
''' unlike a status '''
""" unlike a status """
status = models.Status.objects.get(id=status_id)
try:
favorite = models.Favorite.objects.get(
status=status,
user=request.user
)
favorite = models.Favorite.objects.get(status=status, user=request.user)
except models.Favorite.DoesNotExist:
# can't find that status, idk
return HttpResponseNotFound()
favorite.delete()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Boost(View):
''' boost a status '''
""" boost a status """
def post(self, request, status_id):
''' boost a status '''
""" boost a status """
status = models.Status.objects.get(id=status_id)
# is it boostable?
if not status.boostable:
return HttpResponseBadRequest()
if models.Boost.objects.filter(
boosted_status=status, user=request.user).exists():
boosted_status=status, user=request.user
).exists():
# you already boosted that.
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
models.Boost.objects.create(
boosted_status=status,
privacy=status.privacy,
user=request.user,
)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Unboost(View):
''' boost a status '''
""" boost a status """
def post(self, request, status_id):
''' boost a status '''
""" boost a status """
status = models.Status.objects.get(id=status_id)
boost = models.Boost.objects.filter(
boosted_status=status, user=request.user
).first()
boost.delete()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))

View File

@ -1,4 +1,4 @@
''' invites when registration is closed '''
""" invites when registration is closed """
from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest
@ -12,31 +12,36 @@ from bookwyrm.settings import PAGE_LENGTH
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required('bookwyrm.create_invites', raise_exception=True),
name='dispatch')
permission_required("bookwyrm.create_invites", raise_exception=True),
name="dispatch",
)
class ManageInvites(View):
''' create invites '''
""" create invites """
def get(self, request):
''' invite management page '''
""" invite management page """
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
paginated = Paginator(models.SiteInvite.objects.filter(
user=request.user
).order_by('-created_date'), PAGE_LENGTH)
paginated = Paginator(
models.SiteInvite.objects.filter(user=request.user).order_by(
"-created_date"
),
PAGE_LENGTH,
)
data = {
'invites': paginated.page(page),
'form': forms.CreateInviteForm(),
"invites": paginated.page(page),
"form": forms.CreateInviteForm(),
}
return TemplateResponse(request, 'settings/manage_invites.html', data)
return TemplateResponse(request, "settings/manage_invites.html", data)
def post(self, request):
''' creates an invite database entry '''
""" creates an invite database entry """
form = forms.CreateInviteForm(request.POST)
if not form.is_valid():
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
@ -45,29 +50,30 @@ class ManageInvites(View):
invite.user = request.user
invite.save()
paginated = Paginator(models.SiteInvite.objects.filter(
user=request.user
).order_by('-created_date'), PAGE_LENGTH)
data = {
'invites': paginated.page(1),
'form': form
}
return TemplateResponse(request, 'settings/manage_invites.html', data)
paginated = Paginator(
models.SiteInvite.objects.filter(user=request.user).order_by(
"-created_date"
),
PAGE_LENGTH,
)
data = {"invites": paginated.page(1), "form": form}
return TemplateResponse(request, "settings/manage_invites.html", data)
class Invite(View):
''' use an invite to register '''
""" use an invite to register """
def get(self, request, code):
''' endpoint for using an invites '''
""" endpoint for using an invites """
if request.user.is_authenticated:
return redirect('/')
return redirect("/")
invite = get_object_or_404(models.SiteInvite, code=code)
data = {
'register_form': forms.RegisterForm(),
'invite': invite,
'valid': invite.valid() if invite else True,
"register_form": forms.RegisterForm(),
"invite": invite,
"valid": invite.valid() if invite else True,
}
return TemplateResponse(request, 'invite.html', data)
return TemplateResponse(request, "invite.html", data)
# post handling is in views.authentication.Register

View File

@ -1,4 +1,4 @@
''' isbn search view '''
""" isbn search view """
from django.http import HttpResponseNotFound
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
@ -13,17 +13,18 @@ from .helpers import is_api_request
# pylint: disable= no-self-use
class Isbn(View):
''' search a book by isbn '''
""" search a book by isbn """
def get(self, request, isbn):
''' info about a book '''
""" info about a book """
book_results = connector_manager.isbn_local_search(isbn)
if is_api_request(request):
return JsonResponse([r.json() for r in book_results], safe=False)
data = {
'title': 'ISBN Search Results',
'results': book_results,
'query': isbn,
"title": "ISBN Search Results",
"results": book_results,
"query": isbn,
}
return TemplateResponse(request, 'isbn_search_results.html', data)
return TemplateResponse(request, "isbn_search_results.html", data)

View File

@ -1,4 +1,4 @@
''' non-interactive pages '''
""" non-interactive pages """
from django.db.models import Max
from django.template.response import TemplateResponse
from django.views import View
@ -9,38 +9,44 @@ from .feed import Feed
# pylint: disable= no-self-use
class About(View):
''' create invites '''
""" create invites """
def get(self, request):
''' more information about the instance '''
return TemplateResponse(request, 'discover/about.html')
""" more information about the instance """
return TemplateResponse(request, "discover/about.html")
class Home(View):
''' discover page or home feed depending on auth '''
""" discover page or home feed depending on auth """
def get(self, request):
''' this is the same as the feed on the home tab '''
""" this is the same as the feed on the home tab """
if request.user.is_authenticated:
feed_view = Feed.as_view()
return feed_view(request, 'home')
return feed_view(request, "home")
discover_view = Discover.as_view()
return discover_view(request)
class Discover(View):
''' preview of recently reviewed books '''
""" preview of recently reviewed books """
def get(self, request):
''' tiled book activity page '''
books = models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=['public', 'unlisted'],
).exclude(
cover__exact=''
).annotate(
Max('review__published_date')
).order_by('-review__published_date__max')[:6]
""" tiled book activity page """
books = (
models.Edition.objects.filter(
review__published_date__isnull=False,
review__deleted=False,
review__user__local=True,
review__privacy__in=["public", "unlisted"],
)
.exclude(cover__exact="")
.annotate(Max("review__published_date"))
.order_by("-review__published_date__max")[:6]
)
data = {
'register_form': forms.RegisterForm(),
'books': list(set(books)),
"register_form": forms.RegisterForm(),
"books": list(set(books)),
}
return TemplateResponse(request, 'discover/discover.html', data)
return TemplateResponse(request, "discover/discover.html", data)

View File

@ -1,4 +1,4 @@
''' book list views'''
""" book list views"""
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db import IntegrityError
@ -18,51 +18,57 @@ from .helpers import get_user_from_username
# pylint: disable=no-self-use
class Lists(View):
''' book list page '''
""" book list page """
def get(self, request):
''' display a book list '''
""" display a book list """
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
user = request.user if request.user.is_authenticated else None
# hide lists with no approved books
lists = models.List.objects.filter(
~Q(user=user),
).annotate(
item_count=Count('listitem', filter=Q(listitem__approved=True))
).filter(
item_count__gt=0
).distinct().all()
lists = (
models.List.objects.filter(
~Q(user=user),
)
.annotate(item_count=Count("listitem", filter=Q(listitem__approved=True)))
.filter(item_count__gt=0)
.distinct()
.all()
)
lists = privacy_filter(
request.user, lists, privacy_levels=['public', 'followers'])
request.user, lists, privacy_levels=["public", "followers"]
)
paginated = Paginator(lists, 12)
data = {
'lists': paginated.page(page),
'list_form': forms.ListForm(),
'path': '/list',
"lists": paginated.page(page),
"list_form": forms.ListForm(),
"path": "/list",
}
return TemplateResponse(request, 'lists/lists.html', data)
return TemplateResponse(request, "lists/lists.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
# pylint: disable=unused-argument
def post(self, request):
''' create a book_list '''
""" create a book_list """
form = forms.ListForm(request.POST)
if not form.is_valid():
return redirect('lists')
return redirect("lists")
book_list = form.save()
return redirect(book_list.local_path)
class UserLists(View):
''' a user's book list page '''
""" a user's book list page """
def get(self, request, username):
''' display a book list '''
""" display a book list """
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
user = get_user_from_username(request.user, username)
@ -71,19 +77,20 @@ class UserLists(View):
paginated = Paginator(lists, 12)
data = {
'user': user,
'is_self': request.user.id == user.id,
'lists': paginated.page(page),
'list_form': forms.ListForm(),
'path': user.local_path + '/lists',
"user": user,
"is_self": request.user.id == user.id,
"lists": paginated.page(page),
"list_form": forms.ListForm(),
"path": user.local_path + "/lists",
}
return TemplateResponse(request, 'user/lists.html', data)
return TemplateResponse(request, "user/lists.html", data)
class List(View):
''' book list page '''
""" book list page """
def get(self, request, list_id):
''' display a book list '''
""" display a book list """
book_list = get_object_or_404(models.List, id=list_id)
if not object_visible_to_user(request.user, book_list):
return HttpResponseNotFound()
@ -91,7 +98,7 @@ class List(View):
if is_api_request(request):
return ActivitypubResponse(book_list.to_activity(**request.GET))
query = request.GET.get('q')
query = request.GET.get("q")
suggestions = None
if query and request.user.is_authenticated:
# search for books
@ -104,89 +111,85 @@ class List(View):
suggestions = [s.book for s in suggestions[:5]]
if len(suggestions) < 5:
suggestions += [
s.default_edition for s in \
models.Work.objects.filter(
~Q(editions__in=book_list.books.all()),
).order_by('-updated_date')
][:5 - len(suggestions)]
s.default_edition
for s in models.Work.objects.filter(
~Q(editions__in=book_list.books.all()),
).order_by("-updated_date")
][: 5 - len(suggestions)]
data = {
'list': book_list,
'items': book_list.listitem_set.filter(approved=True),
'pending_count': book_list.listitem_set.filter(
approved=False).count(),
'suggested_books': suggestions,
'list_form': forms.ListForm(instance=book_list),
'query': query or ''
"list": book_list,
"items": book_list.listitem_set.filter(approved=True),
"pending_count": book_list.listitem_set.filter(approved=False).count(),
"suggested_books": suggestions,
"list_form": forms.ListForm(instance=book_list),
"query": query or "",
}
return TemplateResponse(request, 'lists/list.html', data)
return TemplateResponse(request, "lists/list.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
# pylint: disable=unused-argument
def post(self, request, list_id):
''' edit a list '''
""" edit a list """
book_list = get_object_or_404(models.List, id=list_id)
form = forms.ListForm(request.POST, instance=book_list)
if not form.is_valid():
return redirect('list', book_list.id)
return redirect("list", book_list.id)
book_list = form.save()
return redirect(book_list.local_path)
class Curate(View):
''' approve or discard list suggestsions '''
@method_decorator(login_required, name='dispatch')
""" approve or discard list suggestsions """
@method_decorator(login_required, name="dispatch")
def get(self, request, list_id):
''' display a pending list '''
""" display a pending list """
book_list = get_object_or_404(models.List, id=list_id)
if not book_list.user == request.user:
# only the creater can curate the list
return HttpResponseNotFound()
data = {
'list': book_list,
'pending': book_list.listitem_set.filter(approved=False),
'list_form': forms.ListForm(instance=book_list),
"list": book_list,
"pending": book_list.listitem_set.filter(approved=False),
"list_form": forms.ListForm(instance=book_list),
}
return TemplateResponse(request, 'lists/curate.html', data)
return TemplateResponse(request, "lists/curate.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
# pylint: disable=unused-argument
def post(self, request, list_id):
''' edit a book_list '''
""" edit a book_list """
book_list = get_object_or_404(models.List, id=list_id)
suggestion = get_object_or_404(
models.ListItem, id=request.POST.get('item'))
approved = request.POST.get('approved') == 'true'
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
approved = request.POST.get("approved") == "true"
if approved:
suggestion.approved = True
suggestion.save()
else:
suggestion.delete()
return redirect('list-curate', book_list.id)
return redirect("list-curate", book_list.id)
@require_POST
def add_book(request, list_id):
''' put a book on a list '''
""" put a book on a list """
book_list = get_object_or_404(models.List, id=list_id)
if not object_visible_to_user(request.user, book_list):
return HttpResponseNotFound()
book = get_object_or_404(models.Edition, id=request.POST.get('book'))
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
# do you have permission to add to the list?
try:
if request.user == book_list.user or book_list.curation == 'open':
if request.user == book_list.user or book_list.curation == "open":
# go ahead and add it
models.ListItem.objects.create(
book=book,
book_list=book_list,
user=request.user,
)
elif book_list.curation == 'curated':
elif book_list.curation == "curated":
# make a pending entry
models.ListItem.objects.create(
approved=False,
@ -201,17 +204,17 @@ def add_book(request, list_id):
# if the book is already on the list, don't flip out
pass
return redirect('list', list_id)
return redirect("list", list_id)
@require_POST
def remove_book(request, list_id):
''' put a book on a list '''
""" put a book on a list """
book_list = get_object_or_404(models.List, id=list_id)
item = get_object_or_404(models.ListItem, id=request.POST.get('item'))
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
if not book_list.user == request.user and not item.user == request.user:
return HttpResponseNotFound()
item.delete()
return redirect('list', list_id)
return redirect("list", list_id)

View File

@ -1,4 +1,4 @@
''' non-interactive pages '''
""" non-interactive pages """
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
@ -7,22 +7,22 @@ from django.views import View
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Notifications(View):
''' notifications view '''
""" notifications view """
def get(self, request):
''' people are interacting with you, get hyped '''
notifications = request.user.notification_set.all() \
.order_by('-created_date')
""" people are interacting with you, get hyped """
notifications = request.user.notification_set.all().order_by("-created_date")
unread = [n.id for n in notifications.filter(read=False)]
data = {
'notifications': notifications,
'unread': unread,
"notifications": notifications,
"unread": unread,
}
notifications.update(read=True)
return TemplateResponse(request, 'notifications.html', data)
return TemplateResponse(request, "notifications.html", data)
def post(self, request):
''' permanently delete notification for user '''
""" permanently delete notification for user """
request.user.notification_set.filter(read=True).delete()
return redirect('/notifications')
return redirect("/notifications")

View File

@ -1,4 +1,4 @@
''' the good stuff! the books! '''
""" the good stuff! the books! """
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views import View
@ -9,11 +9,12 @@ from .helpers import is_bookwyrm_request
# pylint: disable= no-self-use
class Outbox(View):
''' outbox '''
""" outbox """
def get(self, request, username):
''' outbox for the requested user '''
""" outbox for the requested user """
user = get_object_or_404(models.User, localname=username)
filter_type = request.GET.get('type')
filter_type = request.GET.get("type")
if filter_type not in models.status_models:
filter_type = None
@ -23,5 +24,5 @@ class Outbox(View):
filter_type=filter_type,
pure=not is_bookwyrm_request(request)
),
encoder=activitypub.ActivityEncoder
encoder=activitypub.ActivityEncoder,
)

View File

@ -1,4 +1,4 @@
''' class views for password management '''
""" class views for password management """
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
@ -13,21 +13,22 @@ from bookwyrm.emailing import password_reset_email
# pylint: disable= no-self-use
class PasswordResetRequest(View):
''' forgot password flow '''
""" forgot password flow """
def get(self, request):
''' password reset page '''
""" password reset page """
return TemplateResponse(
request,
'password_reset_request.html',
"password_reset_request.html",
)
def post(self, request):
''' create a password reset token '''
email = request.POST.get('email')
""" 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')
return redirect("/password-reset")
# remove any existing password reset cods for this user
models.PasswordReset.objects.filter(user=user).all().delete()
@ -35,16 +36,17 @@ class PasswordResetRequest(View):
# 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)
data = {"message": "Password reset link sent to %s" % email}
return TemplateResponse(request, "password_reset_request.html", data)
class PasswordReset(View):
''' set new password '''
""" set new password """
def get(self, request, code):
''' endpoint for sending invites '''
""" endpoint for sending invites """
if request.user.is_authenticated:
return redirect('/')
return redirect("/")
try:
reset_code = models.PasswordReset.objects.get(code=code)
if not reset_code.valid():
@ -52,50 +54,48 @@ class PasswordReset(View):
except models.PasswordReset.DoesNotExist:
raise PermissionDenied
return TemplateResponse(request, 'password_reset.html')
return TemplateResponse(request, "password_reset.html")
def post(self, request, code):
''' allow a user to change their password through an emailed token '''
""" allow a user to change their password through an emailed token """
try:
reset_code = models.PasswordReset.objects.get(
code=code
)
reset_code = models.PasswordReset.objects.get(code=code)
except models.PasswordReset.DoesNotExist:
data = {'errors': ['Invalid password reset link']}
return TemplateResponse(request, 'password_reset.html', data)
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')
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)
data = {"errors": ["Passwords do not match"]}
return TemplateResponse(request, "password_reset.html", data)
user.set_password(new_password)
user.save(broadcast=False)
login(request, user)
reset_code.delete()
return redirect('/')
return redirect("/")
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class ChangePassword(View):
''' change password as logged in user '''
""" change password as logged in user """
def get(self, request):
''' change password page '''
data = {'user': request.user}
return TemplateResponse(
request, 'preferences/change_password.html', data)
""" change password page """
data = {"user": request.user}
return TemplateResponse(request, "preferences/change_password.html", data)
def post(self, request):
''' allow a user to change their password '''
new_password = request.POST.get('password')
confirm_password = request.POST.get('confirm-password')
""" 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('preferences/password')
return redirect("preferences/password")
request.user.set_password(new_password)
request.user.save(broadcast=False)

View File

@ -1,4 +1,4 @@
''' the good stuff! the books! '''
""" the good stuff! the books! """
import dateutil.parser
from dateutil.parser import ParserError
@ -17,12 +17,9 @@ from .shelf import handle_unshelve
@login_required
@require_POST
def start_reading(request, book_id):
''' begin reading a book '''
""" begin reading a book """
book = get_edition(book_id)
shelf = models.Shelf.objects.filter(
identifier='reading',
user=request.user
).first()
shelf = models.Shelf.objects.filter(identifier="reading", user=request.user).first()
# create a readthrough
readthrough = update_readthrough(request, book=book)
@ -33,36 +30,29 @@ def start_reading(request, book_id):
readthrough.create_update()
# shelve the book
if request.POST.get('reshelve', True):
if request.POST.get("reshelve", True):
try:
current_shelf = models.Shelf.objects.get(
user=request.user,
edition=book
)
current_shelf = models.Shelf.objects.get(user=request.user, edition=book)
handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
models.ShelfBook.objects.create(
book=book, shelf=shelf, user=request.user)
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)
# post about it (if you want)
if request.POST.get('post-status'):
privacy = request.POST.get('privacy')
if request.POST.get("post-status"):
privacy = request.POST.get("privacy")
handle_reading_status(request.user, shelf, book, privacy)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@login_required
@require_POST
def finish_reading(request, book_id):
''' a user completed a book, yay '''
""" a user completed a book, yay """
book = get_edition(book_id)
shelf = models.Shelf.objects.filter(
identifier='read',
user=request.user
).first()
shelf = models.Shelf.objects.filter(identifier="read", user=request.user).first()
# update or create a readthrough
readthrough = update_readthrough(request, book=book)
@ -70,31 +60,27 @@ def finish_reading(request, book_id):
readthrough.save()
# shelve the book
if request.POST.get('reshelve', True):
if request.POST.get("reshelve", True):
try:
current_shelf = models.Shelf.objects.get(
user=request.user,
edition=book
)
current_shelf = models.Shelf.objects.get(user=request.user, edition=book)
handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
models.ShelfBook.objects.create(
book=book, shelf=shelf, user=request.user)
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)
# post about it (if you want)
if request.POST.get('post-status'):
privacy = request.POST.get('privacy')
if request.POST.get("post-status"):
privacy = request.POST.get("privacy")
handle_reading_status(request.user, shelf, book, privacy)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@login_required
@require_POST
def edit_readthrough(request):
''' can't use the form because the dates are too finnicky '''
""" can't use the form because the dates are too finnicky """
readthrough = update_readthrough(request, create=False)
if not readthrough:
return HttpResponseNotFound()
@ -108,40 +94,39 @@ def edit_readthrough(request):
# use default now for date field
readthrough.create_update()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@login_required
@require_POST
def delete_readthrough(request):
''' remove a readthrough '''
readthrough = get_object_or_404(
models.ReadThrough, id=request.POST.get('id'))
""" remove a readthrough """
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.delete()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
@login_required
@require_POST
def create_readthrough(request):
''' can't use the form because the dates are too finnicky '''
book = get_object_or_404(models.Edition, id=request.POST.get('book'))
""" can't use the form because the dates are too finnicky """
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
readthrough = update_readthrough(request, create=True, book=book)
if not readthrough:
return redirect(book.local_path)
readthrough.save()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
def update_readthrough(request, book=None, create=True):
''' updates but does not save dates on a readthrough '''
""" updates but does not save dates on a readthrough """
try:
read_id = request.POST.get('id')
read_id = request.POST.get("id")
if not read_id:
raise models.ReadThrough.DoesNotExist
readthrough = models.ReadThrough.objects.get(id=read_id)
@ -153,7 +138,7 @@ def update_readthrough(request, book=None, create=True):
book=book,
)
start_date = request.POST.get('start_date')
start_date = request.POST.get("start_date")
if start_date:
try:
start_date = timezone.make_aware(dateutil.parser.parse(start_date))
@ -161,16 +146,15 @@ def update_readthrough(request, book=None, create=True):
except ParserError:
pass
finish_date = request.POST.get('finish_date')
finish_date = request.POST.get("finish_date")
if finish_date:
try:
finish_date = timezone.make_aware(
dateutil.parser.parse(finish_date))
finish_date = timezone.make_aware(dateutil.parser.parse(finish_date))
readthrough.finish_date = finish_date
except ParserError:
pass
progress = request.POST.get('progress')
progress = request.POST.get("progress")
if progress:
try:
progress = int(progress)
@ -178,7 +162,7 @@ def update_readthrough(request, book=None, create=True):
except ValueError:
pass
progress_mode = request.POST.get('progress_mode')
progress_mode = request.POST.get("progress_mode")
if progress_mode:
try:
progress_mode = models.ProgressMode(progress_mode)
@ -191,15 +175,16 @@ def update_readthrough(request, book=None, create=True):
return readthrough
@login_required
@require_POST
def delete_progressupdate(request):
''' remove a progress update '''
update = get_object_or_404(models.ProgressUpdate, id=request.POST.get('id'))
""" remove a progress update """
update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id"))
# don't let people edit other people's data
if request.user != update.user:
return HttpResponseBadRequest()
update.delete()
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))

View File

@ -1,38 +1,35 @@
''' serialize user's posts in rss feed '''
""" serialize user's posts in rss feed """
from django.contrib.syndication.views import Feed
from .helpers import get_activity_feed, get_user_from_username
# pylint: disable=no-self-use, unused-argument
class RssFeed(Feed):
''' serialize user's posts in rss feed '''
description_template = 'snippets/rss_content.html'
title_template = 'snippets/rss_title.html'
""" serialize user's posts in rss feed """
description_template = "snippets/rss_content.html"
title_template = "snippets/rss_title.html"
def get_object(self, request, username):
''' the user who's posts get serialized '''
""" the user who's posts get serialized """
return get_user_from_username(request.user, username)
def link(self, obj):
''' link to the user's profile '''
""" link to the user's profile """
return obj.local_path
def title(self, obj):
''' title of the rss feed entry '''
return f'Status updates from {obj.display_name}'
""" title of the rss feed entry """
return f"Status updates from {obj.display_name}"
def items(self, obj):
''' the user's activity feed '''
""" the user's activity feed """
return get_activity_feed(
obj,
privacy=['public', 'unlisted'],
queryset=obj.status_set.select_subclasses()
privacy=["public", "unlisted"],
queryset=obj.status_set.select_subclasses(),
)
def item_link(self, item):
''' link to the status '''
""" link to the status """
return item.local_path

View File

@ -1,4 +1,4 @@
''' search views'''
""" search views"""
import re
from django.contrib.postgres.search import TrigramSimilarity
@ -16,51 +16,63 @@ from .helpers import handle_remote_webfinger
# pylint: disable= no-self-use
class Search(View):
''' search users or books '''
""" search users or books """
def get(self, request):
''' that search bar up top '''
query = request.GET.get('q')
min_confidence = request.GET.get('min_confidence', 0.1)
""" that search bar up top """
query = request.GET.get("q")
min_confidence = request.GET.get("min_confidence", 0.1)
if is_api_request(request):
# only return local book results via json so we don't cascade
book_results = connector_manager.local_search(
query, min_confidence=min_confidence)
query, min_confidence=min_confidence
)
return JsonResponse([r.json() for r in book_results], safe=False)
# use webfinger for mastodon style account@domain.com username
if re.match(r'\B%s' % regex.full_username, query):
if re.match(r"\B%s" % regex.full_username, query):
handle_remote_webfinger(query)
# do a user search
user_results = models.User.viewer_aware_objects(request.user).annotate(
similarity=Greatest(
TrigramSimilarity('username', query),
TrigramSimilarity('localname', query),
user_results = (
models.User.viewer_aware_objects(request.user)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
)
)
).filter(
similarity__gt=0.5,
).order_by('-similarity')[:10]
.filter(
similarity__gt=0.5,
)
.order_by("-similarity")[:10]
)
# any relevent lists?
list_results = privacy_filter(
request.user, models.List.objects,
privacy_levels=['public', 'followers']
).annotate(
similarity=Greatest(
TrigramSimilarity('name', query),
TrigramSimilarity('description', query),
list_results = (
privacy_filter(
request.user,
models.List.objects,
privacy_levels=["public", "followers"],
)
).filter(
similarity__gt=0.1,
).order_by('-similarity')[:10]
.annotate(
similarity=Greatest(
TrigramSimilarity("name", query),
TrigramSimilarity("description", query),
)
)
.filter(
similarity__gt=0.1,
)
.order_by("-similarity")[:10]
)
book_results = connector_manager.search(
query, min_confidence=min_confidence)
book_results = connector_manager.search(query, min_confidence=min_confidence)
data = {
'book_results': book_results,
'user_results': user_results,
'list_results': list_results,
'query': query,
"book_results": book_results,
"user_results": user_results,
"list_results": list_results,
"query": query,
}
return TemplateResponse(request, 'search_results.html', data)
return TemplateResponse(request, "search_results.html", data)

View File

@ -1,4 +1,4 @@
''' shelf views'''
""" shelf views"""
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
@ -15,9 +15,10 @@ from .helpers import handle_reading_status
# pylint: disable= no-self-use
class Shelf(View):
''' shelf page '''
""" shelf page """
def get(self, request, username, shelf_identifier):
''' display a shelf '''
""" display a shelf """
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -34,38 +35,40 @@ class Shelf(View):
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
# make sure the user has permission to view the shelf
if shelf.privacy == 'direct' or \
(shelf.privacy == 'followers' and not follower):
if shelf.privacy == "direct" or (
shelf.privacy == "followers" and not follower
):
return HttpResponseNotFound()
# only show other shelves that should be visible
if follower:
shelves = shelves.filter(privacy__in=['public', 'followers'])
shelves = shelves.filter(privacy__in=["public", "followers"])
else:
shelves = shelves.filter(privacy='public')
shelves = shelves.filter(privacy="public")
if is_api_request(request):
return ActivitypubResponse(shelf.to_activity(**request.GET))
books = models.ShelfBook.objects.filter(
user=user, shelf=shelf
).order_by('-updated_date').all()
books = (
models.ShelfBook.objects.filter(user=user, shelf=shelf)
.order_by("-updated_date")
.all()
)
data = {
'user': user,
'is_self': is_self,
'shelves': shelves.all(),
'shelf': shelf,
'books': [b.book for b in books],
"user": user,
"is_self": is_self,
"shelves": shelves.all(),
"shelf": shelf,
"books": [b.book for b in books],
}
return TemplateResponse(request, 'user/shelf.html', data)
return TemplateResponse(request, "user/shelf.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
# pylint: disable=unused-argument
def post(self, request, username, shelf_identifier):
''' edit a shelf '''
""" edit a shelf """
try:
shelf = request.user.shelf_set.get(identifier=shelf_identifier)
except models.Shelf.DoesNotExist:
@ -73,7 +76,7 @@ class Shelf(View):
if request.user != shelf.user:
return HttpResponseBadRequest()
if not shelf.editable and request.POST.get('name') != shelf.name:
if not shelf.editable and request.POST.get("name") != shelf.name:
return HttpResponseBadRequest()
form = forms.ShelfForm(request.POST, instance=shelf)
@ -84,88 +87,76 @@ class Shelf(View):
def user_shelves_page(request, username):
''' default shelf '''
""" default shelf """
return Shelf.as_view()(request, username, None)
@login_required
@require_POST
def create_shelf(request):
''' user generated shelves '''
""" user generated shelves """
form = forms.ShelfForm(request.POST)
if not form.is_valid():
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
shelf = form.save()
return redirect('/user/%s/shelf/%s' % \
(request.user.localname, shelf.identifier))
return redirect("/user/%s/shelf/%s" % (request.user.localname, shelf.identifier))
@login_required
@require_POST
def delete_shelf(request, shelf_id):
''' user generated shelves '''
""" user generated shelves """
shelf = get_object_or_404(models.Shelf, id=shelf_id)
if request.user != shelf.user or not shelf.editable:
return HttpResponseBadRequest()
shelf.delete()
return redirect('/user/%s/shelves' % request.user.localname)
return redirect("/user/%s/shelves" % request.user.localname)
@login_required
@require_POST
def shelve(request):
''' put a on a user's shelf '''
book = get_edition(request.POST.get('book'))
""" put a on a user's shelf """
book = get_edition(request.POST.get("book"))
desired_shelf = models.Shelf.objects.filter(
identifier=request.POST.get('shelf'),
user=request.user
identifier=request.POST.get("shelf"), user=request.user
).first()
if not desired_shelf:
return HttpResponseNotFound()
if request.POST.get('reshelve', True):
if request.POST.get("reshelve", True):
try:
current_shelf = models.Shelf.objects.get(
user=request.user,
edition=book
)
current_shelf = models.Shelf.objects.get(user=request.user, edition=book)
handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, user=request.user)
models.ShelfBook.objects.create(book=book, shelf=desired_shelf, user=request.user)
# post about "want to read" shelves
if desired_shelf.identifier == 'to-read' and \
request.POST.get('post-status'):
privacy = request.POST.get('privacy') or desired_shelf.privacy
handle_reading_status(
request.user,
desired_shelf,
book,
privacy=privacy
)
if desired_shelf.identifier == "to-read" and request.POST.get("post-status"):
privacy = request.POST.get("privacy") or desired_shelf.privacy
handle_reading_status(request.user, desired_shelf, book, privacy=privacy)
return redirect('/')
return redirect("/")
@login_required
@require_POST
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'])
""" 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"])
handle_unshelve(request.user, book, current_shelf)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
#pylint: disable=unused-argument
# pylint: disable=unused-argument
def handle_unshelve(user, book, shelf):
''' unshelve a book '''
""" unshelve a book """
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
row.delete()

View File

@ -1,4 +1,4 @@
''' manage site settings '''
""" manage site settings """
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import redirect
from django.template.response import TemplateResponse
@ -9,26 +9,27 @@ from bookwyrm import forms, models
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required(
'bookwyrm.edit_instance_settings', raise_exception=True),
name='dispatch')
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
class Site(View):
''' manage things like the instance name '''
""" manage things like the instance name """
def get(self, request):
''' edit form '''
""" edit form """
site = models.SiteSettings.objects.get()
data = {'site_form': forms.SiteForm(instance=site)}
return TemplateResponse(request, 'settings/site.html', data)
data = {"site_form": forms.SiteForm(instance=site)}
return TemplateResponse(request, "settings/site.html", data)
def post(self, request):
''' edit the site settings '''
""" edit the site settings """
site = models.SiteSettings.objects.get()
form = forms.SiteForm(request.POST, instance=site)
if not form.is_valid():
data = {'site_form': form}
return TemplateResponse(request, 'settings/site.html', data)
data = {"site_form": form}
return TemplateResponse(request, "settings/site.html", data)
form.save()
return redirect('settings-site')
return redirect("settings-site")

View File

@ -1,4 +1,4 @@
''' what are we here for if not for posting '''
""" what are we here for if not for posting """
import re
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
@ -16,19 +16,20 @@ from .helpers import handle_remote_webfinger
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class CreateStatus(View):
''' the view for *posting* '''
""" the view for *posting* """
def post(self, request, status_type):
''' create status of whatever type '''
""" create status of whatever type """
status_type = status_type[0].upper() + status_type[1:]
try:
form = getattr(forms, '%sForm' % status_type)(request.POST)
form = getattr(forms, "%sForm" % status_type)(request.POST)
except AttributeError:
return HttpResponseBadRequest()
if not form.is_valid():
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
status = form.save(commit=False)
if not status.sensitive and status.content_warning:
@ -44,10 +45,10 @@ class CreateStatus(View):
# turn the mention into a link
content = re.sub(
r'%s([^@]|$)' % mention_text,
r'<a href="%s">%s</a>\g<1>' % \
(mention_user.remote_id, mention_text),
content)
r"%s([^@]|$)" % mention_text,
r'<a href="%s">%s</a>\g<1>' % (mention_user.remote_id, mention_text),
content,
)
# add reply parent to mentions
if status.reply_parent:
status.mention_users.add(status.reply_parent.user)
@ -59,17 +60,18 @@ class CreateStatus(View):
if not isinstance(status, models.GeneratedNote):
status.content = to_markdown(content)
# do apply formatting to quotes
if hasattr(status, 'quote'):
if hasattr(status, "quote"):
status.quote = to_markdown(status.quote)
status.save(created=True)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
class DeleteStatus(View):
''' tombstone that bad boy '''
""" tombstone that bad boy """
def post(self, request, status_id):
''' delete and tombstone a status '''
""" delete and tombstone a status """
status = get_object_or_404(models.Status, id=status_id)
# don't let people delete other people's statuses
@ -78,16 +80,17 @@ class DeleteStatus(View):
# perform deletion
delete_status(status)
return redirect(request.headers.get('Referer', '/'))
return redirect(request.headers.get("Referer", "/"))
def find_mentions(content):
''' detect @mentions in raw status content '''
""" detect @mentions in raw status content """
for match in re.finditer(regex.strict_username, content):
username = match.group().strip().split('@')[1:]
username = match.group().strip().split("@")[1:]
if len(username) == 1:
# this looks like a local user (@user), fill in the domain
username.append(DOMAIN)
username = '@'.join(username)
username = "@".join(username)
mention_user = handle_remote_webfinger(username)
if not mention_user:
@ -97,15 +100,16 @@ def find_mentions(content):
def format_links(content):
''' detect and format links '''
""" detect and format links """
return re.sub(
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
regex.domain,
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % regex.domain,
r'\g<1><a href="\g<2>">\g<3></a>',
content)
content,
)
def to_markdown(content):
''' catch links and convert to markdown '''
""" catch links and convert to markdown """
content = markdown(content)
content = format_links(content)
# sanitize resulting html

View File

@ -1,4 +1,4 @@
''' tagging views'''
""" tagging views"""
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
@ -12,34 +12,35 @@ from .helpers import is_api_request
# pylint: disable= no-self-use
class Tag(View):
''' tag page '''
""" tag page """
def get(self, request, tag_id):
''' see books related to a tag '''
""" see books related to a tag """
tag_obj = get_object_or_404(models.Tag, identifier=tag_id)
if is_api_request(request):
return ActivitypubResponse(
tag_obj.to_activity(**request.GET))
return ActivitypubResponse(tag_obj.to_activity(**request.GET))
books = models.Edition.objects.filter(
usertag__tag__identifier=tag_id
).distinct()
data = {
'books': books,
'tag': tag_obj,
"books": books,
"tag": tag_obj,
}
return TemplateResponse(request, 'tag.html', data)
return TemplateResponse(request, "tag.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class AddTag(View):
''' add a tag to a book '''
""" add a tag to a book """
def post(self, request):
''' tag a book '''
""" 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')
name = request.POST.get("name")
book_id = request.POST.get("book")
book = get_object_or_404(models.Edition, id=book_id)
tag_obj, _ = models.Tag.objects.get_or_create(
name=name,
@ -50,21 +51,23 @@ class AddTag(View):
tag=tag_obj,
)
return redirect('/book/%s' % book_id)
return redirect("/book/%s" % book_id)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class RemoveTag(View):
''' remove a user's tag from a book '''
""" remove a user's tag from a book """
def post(self, request):
''' untag a book '''
name = request.POST.get('name')
""" untag a book """
name = request.POST.get("name")
tag_obj = get_object_or_404(models.Tag, name=name)
book_id = request.POST.get('book')
book_id = request.POST.get("book")
book = get_object_or_404(models.Edition, id=book_id)
user_tag = get_object_or_404(
models.UserTag, tag=tag_obj, book=book, user=request.user)
models.UserTag, tag=tag_obj, book=book, user=request.user
)
user_tag.delete()
return redirect('/book/%s' % book_id)
return redirect("/book/%s" % book_id)

View File

@ -1,17 +1,20 @@
''' endpoints for getting updates about activity '''
""" endpoints for getting updates about activity """
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class Updates(View):
''' so the app can poll '''
""" so the app can poll """
def get(self, request):
''' any notifications waiting? '''
return JsonResponse({
'notifications': request.user.notification_set.filter(
read=False
).count(),
})
""" any notifications waiting? """
return JsonResponse(
{
"notifications": request.user.notification_set.filter(
read=False
).count(),
}
)

View File

@ -1,4 +1,4 @@
''' non-interactive pages '''
""" non-interactive pages """
from io import BytesIO
from uuid import uuid4
from PIL import Image
@ -22,9 +22,10 @@ from .helpers import is_blocked, object_visible_to_user
# pylint: disable= no-self-use
class User(View):
''' user profile page '''
""" user profile page """
def get(self, request, username):
''' profile page for a user '''
""" profile page for a user """
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -40,7 +41,7 @@ class User(View):
# otherwise we're at a UI view
try:
page = int(request.GET.get('page', 1))
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
@ -52,19 +53,21 @@ class User(View):
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
if follower:
shelves = shelves.filter(privacy__in=['public', 'followers'])
shelves = shelves.filter(privacy__in=["public", "followers"])
else:
shelves = shelves.filter(privacy='public')
shelves = shelves.filter(privacy="public")
for user_shelf in shelves.all():
if not user_shelf.books.count():
continue
shelf_preview.append({
'name': user_shelf.name,
'local_path': user_shelf.local_path,
'books': user_shelf.books.all()[:3],
'size': user_shelf.books.count(),
})
shelf_preview.append(
{
"name": user_shelf.name,
"local_path": user_shelf.local_path,
"books": user_shelf.books.all()[:3],
"size": user_shelf.books.count(),
}
)
if len(shelf_preview) > 2:
break
@ -75,24 +78,27 @@ class User(View):
)
paginated = Paginator(activities, PAGE_LENGTH)
goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year).first()
user=user, year=timezone.now().year
).first()
if not object_visible_to_user(request.user, goal):
goal = None
data = {
'user': user,
'is_self': is_self,
'shelves': shelf_preview,
'shelf_count': shelves.count(),
'activities': paginated.page(page),
'goal': goal,
"user": user,
"is_self": is_self,
"shelves": shelf_preview,
"shelf_count": shelves.count(),
"activities": paginated.page(page),
"goal": goal,
}
return TemplateResponse(request, 'user/user.html', data)
return TemplateResponse(request, "user/user.html", data)
class Followers(View):
''' list of followers view '''
""" list of followers view """
def get(self, request, username):
''' list of followers '''
""" list of followers """
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -103,20 +109,21 @@ class Followers(View):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
user.to_followers_activity(**request.GET))
return ActivitypubResponse(user.to_followers_activity(**request.GET))
data = {
'user': user,
'is_self': request.user.id == user.id,
'followers': user.followers.all(),
"user": user,
"is_self": request.user.id == user.id,
"followers": user.followers.all(),
}
return TemplateResponse(request, 'user/followers.html', data)
return TemplateResponse(request, "user/followers.html", data)
class Following(View):
''' list of following view '''
""" list of following view """
def get(self, request, username):
''' list of followers '''
""" list of followers """
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
@ -127,46 +134,45 @@ class Following(View):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
user.to_following_activity(**request.GET))
return ActivitypubResponse(user.to_following_activity(**request.GET))
data = {
'user': user,
'is_self': request.user.id == user.id,
'following': user.following.all(),
"user": user,
"is_self": request.user.id == user.id,
"following": user.following.all(),
}
return TemplateResponse(request, 'user/following.html', data)
return TemplateResponse(request, "user/following.html", data)
@method_decorator(login_required, name='dispatch')
@method_decorator(login_required, name="dispatch")
class EditUser(View):
''' edit user view '''
""" edit user view """
def get(self, request):
''' edit profile page for a user '''
""" edit profile page for a user """
data = {
'form': forms.EditUserForm(instance=request.user),
'user': request.user,
"form": forms.EditUserForm(instance=request.user),
"user": request.user,
}
return TemplateResponse(request, 'preferences/edit_user.html', data)
return TemplateResponse(request, "preferences/edit_user.html", data)
def post(self, request):
''' les get fancy with images '''
form = forms.EditUserForm(
request.POST, request.FILES, instance=request.user)
""" les get fancy with images """
form = forms.EditUserForm(request.POST, request.FILES, instance=request.user)
if not form.is_valid():
data = {'form': form, 'user': request.user}
return TemplateResponse(request, 'preferences/edit_user.html', data)
data = {"form": form, "user": request.user}
return TemplateResponse(request, "preferences/edit_user.html", data)
user = form.save(commit=False)
if 'avatar' in form.files:
if "avatar" in form.files:
# crop and resize avatar upload
image = Image.open(form.files['avatar'])
image = Image.open(form.files["avatar"])
image = crop_avatar(image)
# set the name to a hash
extension = form.files['avatar'].name.split('.')[-1]
filename = '%s.%s' % (uuid4(), extension)
extension = form.files["avatar"].name.split(".")[-1]
filename = "%s.%s" % (uuid4(), extension)
user.avatar.save(filename, image)
user.save()
@ -174,22 +180,27 @@ class EditUser(View):
def crop_avatar(image):
''' reduce the size and make an avatar square '''
""" reduce the size and make an avatar square """
target_size = 120
width, height = image.size
thumbnail_scale = height / (width / target_size) if height > width \
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))
))
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)
return ContentFile(output.getvalue())