Merge branch 'main' into create-book
This commit is contained in:
@ -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
|
||||
|
@ -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("/")
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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", "/"))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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", "/"))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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", "/"))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
}
|
||||
)
|
||||
|
@ -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())
|
||||
|
Reference in New Issue
Block a user