Merge branch 'main' into suggestions-redis
This commit is contained in:
@ -6,6 +6,8 @@ from .books import Book, EditBook, ConfirmEditBook, Editions
|
||||
from .books import upload_cover, add_description, switch_edition, resolve_book
|
||||
from .directory import Directory
|
||||
from .federation import Federation, FederatedServer
|
||||
from .federation import AddFederatedServer, ImportServerBlocklist
|
||||
from .federation import block_server, unblock_server
|
||||
from .feed import DirectMessage, Feed, Replies, Status
|
||||
from .follow import follow, unfollow
|
||||
from .follow import accept_follow_request, delete_follow_request
|
||||
@ -23,7 +25,7 @@ from .notifications import Notifications
|
||||
from .outbox import Outbox
|
||||
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
||||
from .reading import start_reading, finish_reading, delete_progressupdate
|
||||
from .reports import Report, Reports, make_report, resolve_report, deactivate_user
|
||||
from .reports import Report, Reports, make_report, resolve_report, suspend_user
|
||||
from .rss_feed import RssFeed
|
||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||
from .search import Search
|
||||
@ -32,8 +34,7 @@ from .shelf import create_shelf, delete_shelf
|
||||
from .shelf import shelve, unshelve
|
||||
from .site import Site
|
||||
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
||||
from .tag import Tag, AddTag, RemoveTag
|
||||
from .updates import get_notification_count, get_unread_status_count
|
||||
from .user import User, EditUser, Followers, Following
|
||||
from .user_admin import UserAdmin
|
||||
from .user_admin import UserAdmin, UserAdminList
|
||||
from .wellknown import *
|
||||
|
@ -16,10 +16,10 @@ from bookwyrm.settings import DOMAIN
|
||||
# pylint: disable= no-self-use
|
||||
@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("/")
|
||||
# sene user to the login page
|
||||
@ -30,7 +30,7 @@ class Login(View):
|
||||
return TemplateResponse(request, "login.html", data)
|
||||
|
||||
def post(self, request):
|
||||
""" authentication action """
|
||||
"""authentication action"""
|
||||
if request.user.is_authenticated:
|
||||
return redirect("/")
|
||||
login_form = forms.LoginForm(request.POST)
|
||||
@ -61,10 +61,10 @@ class Login(View):
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@ -117,9 +117,9 @@ class Register(View):
|
||||
|
||||
@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("/")
|
||||
|
@ -13,10 +13,10 @@ 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):
|
||||
@ -37,16 +37,16 @@ class Author(View):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
@ -12,14 +12,14 @@ from bookwyrm import models
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Block(View):
|
||||
""" blocking users """
|
||||
"""blocking users"""
|
||||
|
||||
def get(self, request):
|
||||
""" list of blocked users? """
|
||||
"""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
|
||||
@ -30,7 +30,7 @@ class Block(View):
|
||||
@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(
|
||||
|
@ -1,6 +1,7 @@
|
||||
""" the good stuff! the books! """
|
||||
from uuid import uuid4
|
||||
|
||||
from dateutil.parser import parse as dateparse
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
from django.core.files.base import ContentFile
|
||||
@ -10,6 +11,7 @@ from django.db.models import Avg, Q
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.datastructures import MultiValueDictKeyError
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
@ -24,15 +26,10 @@ from .helpers import is_api_request, get_edition, privacy_filter
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
class Book(View):
|
||||
""" a book! this is the stuff """
|
||||
|
||||
def get(self, request, book_id):
|
||||
""" info about a book """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
"""a book! this is the stuff"""
|
||||
|
||||
def get(self, request, book_id, user_statuses=False):
|
||||
"""info about a book"""
|
||||
try:
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
except models.Book.DoesNotExist:
|
||||
@ -43,29 +40,41 @@ class Book(View):
|
||||
|
||||
if isinstance(book, models.Work):
|
||||
book = book.get_default_edition()
|
||||
if not book:
|
||||
if not book or not book.parent_work:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
work = book.parent_work
|
||||
if not work:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# all reviews for the book
|
||||
reviews = models.Review.objects.filter(book__in=work.editions.all())
|
||||
reviews = privacy_filter(request.user, reviews)
|
||||
reviews = privacy_filter(
|
||||
request.user, models.Review.objects.filter(book__in=work.editions.all())
|
||||
)
|
||||
|
||||
# the reviews to show
|
||||
paginated = Paginator(
|
||||
reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH
|
||||
)
|
||||
reviews_page = paginated.page(page)
|
||||
if user_statuses and request.user.is_authenticated:
|
||||
if user_statuses == "review":
|
||||
queryset = book.review_set
|
||||
elif user_statuses == "comment":
|
||||
queryset = book.comment_set
|
||||
else:
|
||||
queryset = book.quotation_set
|
||||
queryset = queryset.filter(user=request.user)
|
||||
else:
|
||||
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
data = {
|
||||
"book": book,
|
||||
"statuses": paginated.get_page(request.GET.get("page")),
|
||||
"review_count": reviews.count(),
|
||||
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"lists": privacy_filter(
|
||||
request.user, book.list_set.filter(listitem__approved=True)
|
||||
),
|
||||
}
|
||||
|
||||
user_tags = readthroughs = user_shelves = other_edition_shelves = []
|
||||
if request.user.is_authenticated:
|
||||
user_tags = models.UserTag.objects.filter(
|
||||
book=book, user=request.user
|
||||
).values_list("tag__identifier", flat=True)
|
||||
|
||||
readthroughs = models.ReadThrough.objects.filter(
|
||||
user=request.user,
|
||||
book=book,
|
||||
@ -75,31 +84,24 @@ class Book(View):
|
||||
readthrough.progress_updates = (
|
||||
readthrough.progressupdate_set.all().order_by("-updated_date")
|
||||
)
|
||||
data["readthroughs"] = readthroughs
|
||||
|
||||
user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book)
|
||||
data["user_shelves"] = models.ShelfBook.objects.filter(
|
||||
user=request.user, book=book
|
||||
)
|
||||
|
||||
other_edition_shelves = models.ShelfBook.objects.filter(
|
||||
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
|
||||
~Q(book=book),
|
||||
user=request.user,
|
||||
book__parent_work=book.parent_work,
|
||||
)
|
||||
|
||||
data = {
|
||||
"book": book,
|
||||
"reviews": reviews_page,
|
||||
"review_count": reviews.count(),
|
||||
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"tags": models.UserTag.objects.filter(book=book),
|
||||
"lists": privacy_filter(
|
||||
request.user, book.list_set.filter(listitem__approved=True)
|
||||
),
|
||||
"user_tags": user_tags,
|
||||
"user_shelves": user_shelves,
|
||||
"other_edition_shelves": other_edition_shelves,
|
||||
"readthroughs": readthroughs,
|
||||
"path": "/book/%s" % book_id,
|
||||
}
|
||||
data["user_statuses"] = {
|
||||
"review_count": book.review_set.filter(user=request.user).count(),
|
||||
"comment_count": book.comment_set.filter(user=request.user).count(),
|
||||
"quotation_count": book.quotation_set.filter(user=request.user).count(),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "book/book.html", data)
|
||||
|
||||
|
||||
@ -108,10 +110,10 @@ class Book(View):
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
)
|
||||
class EditBook(View):
|
||||
""" edit a book """
|
||||
"""edit a book"""
|
||||
|
||||
def get(self, request, book_id=None):
|
||||
""" info about a book """
|
||||
"""info about a book"""
|
||||
book = None
|
||||
if book_id:
|
||||
book = get_edition(book_id)
|
||||
@ -121,7 +123,7 @@ class EditBook(View):
|
||||
return TemplateResponse(request, "book/edit_book.html", data)
|
||||
|
||||
def post(self, request, book_id=None):
|
||||
""" edit a book cool """
|
||||
"""edit a book cool"""
|
||||
# returns None if no match is found
|
||||
book = models.Edition.objects.filter(id=book_id).first()
|
||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||
@ -172,6 +174,20 @@ class EditBook(View):
|
||||
data["confirm_mode"] = True
|
||||
# this isn't preserved because it isn't part of the form obj
|
||||
data["remove_authors"] = request.POST.getlist("remove_authors")
|
||||
# make sure the dates are passed in as datetime, they're currently a string
|
||||
# QueryDicts are immutable, we need to copy
|
||||
formcopy = data["form"].data.copy()
|
||||
try:
|
||||
formcopy["first_published_date"] = dateparse(
|
||||
formcopy["first_published_date"]
|
||||
)
|
||||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
try:
|
||||
formcopy["published_date"] = dateparse(formcopy["published_date"])
|
||||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
data["form"].data = formcopy
|
||||
return TemplateResponse(request, "book/edit_book.html", data)
|
||||
|
||||
remove_authors = request.POST.getlist("remove_authors")
|
||||
@ -193,10 +209,10 @@ class EditBook(View):
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
)
|
||||
class ConfirmEditBook(View):
|
||||
""" confirm edits to a book """
|
||||
"""confirm edits to a book"""
|
||||
|
||||
def post(self, request, book_id=None):
|
||||
""" edit a book cool """
|
||||
"""edit a book cool"""
|
||||
# returns None if no match is found
|
||||
book = models.Edition.objects.filter(id=book_id).first()
|
||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||
@ -244,17 +260,12 @@ class ConfirmEditBook(View):
|
||||
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(work.to_edition_list(**request.GET))
|
||||
filters = {}
|
||||
@ -264,12 +275,12 @@ class Editions(View):
|
||||
if request.GET.get("format"):
|
||||
filters["physical_format__iexact"] = request.GET.get("format")
|
||||
|
||||
editions = work.editions.order_by("-edition_rank").all()
|
||||
editions = work.editions.order_by("-edition_rank")
|
||||
languages = set(sum([e.languages for e in editions], []))
|
||||
|
||||
paginated = Paginator(editions.filter(**filters).all(), PAGE_LENGTH)
|
||||
paginated = Paginator(editions.filter(**filters), PAGE_LENGTH)
|
||||
data = {
|
||||
"editions": paginated.page(page),
|
||||
"editions": paginated.get_page(request.GET.get("page")),
|
||||
"work": work,
|
||||
"languages": languages,
|
||||
"formats": set(
|
||||
@ -282,7 +293,7 @@ class Editions(View):
|
||||
@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)
|
||||
book.last_edited_by = request.user
|
||||
|
||||
@ -305,7 +316,7 @@ def upload_cover(request, book_id):
|
||||
|
||||
|
||||
def set_cover_from_url(url):
|
||||
""" load it from a url """
|
||||
"""load it from a url"""
|
||||
image_file = get_image(url)
|
||||
if not image_file:
|
||||
return None
|
||||
@ -318,7 +329,7 @@ def set_cover_from_url(url):
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
def add_description(request, book_id):
|
||||
""" upload a new cover """
|
||||
"""upload a new cover"""
|
||||
if not request.method == "POST":
|
||||
return redirect("/")
|
||||
|
||||
@ -335,7 +346,7 @@ def add_description(request, book_id):
|
||||
|
||||
@require_POST
|
||||
def resolve_book(request):
|
||||
""" figure out the local path to a book from a 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)
|
||||
@ -347,7 +358,7 @@ def resolve_book(request):
|
||||
@require_POST
|
||||
@transaction.atomic
|
||||
def switch_edition(request):
|
||||
""" switch your copy of a book to a different 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(
|
||||
|
@ -11,16 +11,10 @@ from bookwyrm import suggested_users
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Directory(View):
|
||||
""" display of known bookwyrm users """
|
||||
"""display of known bookwyrm users"""
|
||||
|
||||
def get(self, request):
|
||||
""" lets see your cute faces """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
# filters
|
||||
"""lets see your cute faces"""
|
||||
filters = {}
|
||||
software = request.GET.get("software")
|
||||
if not software or software == "bookwyrm":
|
||||
@ -39,12 +33,12 @@ class Directory(View):
|
||||
paginated = Paginator(users, 12)
|
||||
|
||||
data = {
|
||||
"users": paginated.page(page),
|
||||
"users": paginated.get_page(request.GET.get("page")),
|
||||
}
|
||||
return TemplateResponse(request, "directory/directory.html", data)
|
||||
|
||||
def post(self, request):
|
||||
""" join the directory """
|
||||
"""join the directory"""
|
||||
request.user.discoverable = True
|
||||
request.user.save(update_fields=["discoverable"])
|
||||
return redirect("directory")
|
||||
|
@ -1,12 +1,15 @@
|
||||
""" manage federated servers """
|
||||
import json
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
||||
|
||||
@ -17,37 +20,92 @@ from bookwyrm.settings import PAGE_LENGTH
|
||||
name="dispatch",
|
||||
)
|
||||
class Federation(View):
|
||||
""" what servers do we federate with """
|
||||
"""what servers do we federate with"""
|
||||
|
||||
def get(self, request):
|
||||
""" list of servers """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
servers = models.FederatedServer.objects.all()
|
||||
"""list of servers"""
|
||||
servers = models.FederatedServer.objects
|
||||
|
||||
sort = request.GET.get("sort")
|
||||
sort_fields = ["created_date", "application_type", "server_name"]
|
||||
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
servers = servers.order_by(sort)
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
sort = "created_date"
|
||||
servers = servers.order_by(sort)
|
||||
|
||||
paginated = Paginator(servers, PAGE_LENGTH)
|
||||
data = {"servers": paginated.page(page), "sort": sort}
|
||||
|
||||
data = {
|
||||
"servers": paginated.get_page(request.GET.get("page")),
|
||||
"sort": sort,
|
||||
"form": forms.ServerForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federation.html", data)
|
||||
|
||||
|
||||
class AddFederatedServer(View):
|
||||
"""manually add a server"""
|
||||
|
||||
def get(self, request):
|
||||
"""add server form"""
|
||||
data = {"form": forms.ServerForm()}
|
||||
return TemplateResponse(request, "settings/edit_server.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""add a server from the admin panel"""
|
||||
form = forms.ServerForm(request.POST)
|
||||
if not form.is_valid():
|
||||
data = {"form": form}
|
||||
return TemplateResponse(request, "settings/edit_server.html", data)
|
||||
server = form.save()
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.control_federation", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class ImportServerBlocklist(View):
|
||||
"""manually add a server"""
|
||||
|
||||
def get(self, request):
|
||||
"""add server form"""
|
||||
return TemplateResponse(request, "settings/server_blocklist.html")
|
||||
|
||||
def post(self, request):
|
||||
"""add a server from the admin panel"""
|
||||
json_data = json.load(request.FILES["json_file"])
|
||||
failed = []
|
||||
success_count = 0
|
||||
for item in json_data:
|
||||
server_name = item.get("instance")
|
||||
if not server_name:
|
||||
failed.append(item)
|
||||
continue
|
||||
info_link = item.get("url")
|
||||
|
||||
with transaction.atomic():
|
||||
server, _ = models.FederatedServer.objects.get_or_create(
|
||||
server_name=server_name,
|
||||
)
|
||||
server.notes = info_link
|
||||
server.save()
|
||||
server.block()
|
||||
success_count += 1
|
||||
data = {"failed": failed, "succeeded": success_count}
|
||||
return TemplateResponse(request, "settings/server_blocklist.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.control_federation", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class FederatedServer(View):
|
||||
""" views for handling a specific federated server """
|
||||
"""views for handling a specific federated server"""
|
||||
|
||||
def get(self, request, server):
|
||||
""" load a server """
|
||||
"""load a server"""
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
users = server.user_set
|
||||
data = {
|
||||
@ -61,3 +119,32 @@ class FederatedServer(View):
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federated_server.html", data)
|
||||
|
||||
def post(self, request, server): # pylint: disable=unused-argument
|
||||
"""update note"""
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
server.notes = request.POST.get("notes")
|
||||
server.save()
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.control_federation", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def block_server(request, server):
|
||||
"""block a server"""
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
server.block()
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.control_federation", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def unblock_server(request, server):
|
||||
"""unblock a server"""
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
server.unblock()
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
@ -13,21 +13,16 @@ from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH, STREAMS
|
||||
from bookwyrm.suggested_users import suggested_users
|
||||
from .helpers import get_user_from_username, privacy_filter
|
||||
from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user
|
||||
from .helpers import is_api_request, is_bookwyrm_request
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Feed(View):
|
||||
""" activity stream """
|
||||
"""activity stream"""
|
||||
|
||||
def get(self, request, tab):
|
||||
""" user's homepage with activity feed """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
"""user's homepage with activity feed"""
|
||||
if not tab in STREAMS:
|
||||
tab = "home"
|
||||
|
||||
@ -40,7 +35,7 @@ class Feed(View):
|
||||
**feed_page_data(request.user),
|
||||
**{
|
||||
"user": request.user,
|
||||
"activities": paginated.page(page),
|
||||
"activities": paginated.get_page(request.GET.get("page")),
|
||||
"suggested_users": suggestions,
|
||||
"tab": tab,
|
||||
"goal_form": forms.GoalForm(),
|
||||
@ -52,15 +47,10 @@ class Feed(View):
|
||||
|
||||
@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 """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
"""like a feed but for dms only"""
|
||||
# remove fancy subclasses of status, keep just good ol' notes
|
||||
queryset = models.Status.objects.filter(
|
||||
review__isnull=True,
|
||||
@ -83,13 +73,12 @@ class DirectMessage(View):
|
||||
).order_by("-published_date")
|
||||
|
||||
paginated = Paginator(activities, PAGE_LENGTH)
|
||||
activity_page = paginated.page(page)
|
||||
data = {
|
||||
**feed_page_data(request.user),
|
||||
**{
|
||||
"user": request.user,
|
||||
"partner": user,
|
||||
"activities": activity_page,
|
||||
"activities": paginated.get_page(request.GET.get("page")),
|
||||
"path": "/direct-messages",
|
||||
},
|
||||
}
|
||||
@ -97,16 +86,16 @@ class DirectMessage(View):
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
except (ValueError, models.Status.DoesNotExist):
|
||||
except (ValueError, models.Status.DoesNotExist, models.User.DoesNotExist):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# the url should have the poster's username in it
|
||||
@ -114,7 +103,7 @@ class Status(View):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# make sure the user is authorized to see the status
|
||||
if not object_visible_to_user(request.user, status):
|
||||
if not status.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if is_api_request(request):
|
||||
@ -132,10 +121,10 @@ class Status(View):
|
||||
|
||||
|
||||
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()
|
||||
@ -150,7 +139,7 @@ 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 {}
|
||||
|
||||
@ -163,7 +152,7 @@ def feed_page_data(user):
|
||||
|
||||
|
||||
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)]
|
||||
suggested_books = []
|
||||
@ -175,7 +164,7 @@ def get_suggested_books(user, max_books=5):
|
||||
)
|
||||
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")[:limit]
|
||||
if not shelf_books:
|
||||
continue
|
||||
shelf_preview = {
|
||||
|
@ -12,7 +12,7 @@ from .helpers import get_user_from_username
|
||||
@login_required
|
||||
@require_POST
|
||||
def follow(request):
|
||||
""" follow another user, here or abroad """
|
||||
"""follow another user, here or abroad"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
to_follow = get_user_from_username(request.user, username)
|
||||
@ -33,7 +33,7 @@ def follow(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def unfollow(request):
|
||||
""" unfollow a user """
|
||||
"""unfollow a user"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
to_unfollow = get_user_from_username(request.user, username)
|
||||
@ -61,7 +61,7 @@ def unfollow(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def accept_follow_request(request):
|
||||
""" a user accepts a follow request """
|
||||
"""a user accepts a follow request"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
requester = get_user_from_username(request.user, username)
|
||||
@ -83,7 +83,7 @@ def accept_follow_request(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_follow_request(request):
|
||||
""" a user rejects a follow request """
|
||||
"""a user rejects a follow request"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
requester = get_user_from_username(request.user, username)
|
||||
|
@ -19,12 +19,12 @@ from .user import save_user_form
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class GetStartedProfile(View):
|
||||
""" tell us about yourself """
|
||||
"""tell us about yourself"""
|
||||
|
||||
next_view = "get-started-books"
|
||||
|
||||
def get(self, request):
|
||||
""" basic profile info """
|
||||
"""basic profile info"""
|
||||
data = {
|
||||
"form": forms.LimitedEditUserForm(instance=request.user),
|
||||
"next": self.next_view,
|
||||
@ -32,7 +32,7 @@ class GetStartedProfile(View):
|
||||
return TemplateResponse(request, "get_started/profile.html", data)
|
||||
|
||||
def post(self, request):
|
||||
""" update your profile """
|
||||
"""update your profile"""
|
||||
form = forms.LimitedEditUserForm(
|
||||
request.POST, request.FILES, instance=request.user
|
||||
)
|
||||
@ -45,12 +45,12 @@ class GetStartedProfile(View):
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class GetStartedBooks(View):
|
||||
""" name a book, any book, we gotta start somewhere """
|
||||
"""name a book, any book, we gotta start somewhere"""
|
||||
|
||||
next_view = "get-started-users"
|
||||
|
||||
def get(self, request):
|
||||
""" info about a book """
|
||||
"""info about a book"""
|
||||
query = request.GET.get("query")
|
||||
book_results = popular_books = []
|
||||
if query:
|
||||
@ -81,7 +81,7 @@ class GetStartedBooks(View):
|
||||
return TemplateResponse(request, "get_started/books.html", data)
|
||||
|
||||
def post(self, request):
|
||||
""" shelve some books """
|
||||
"""shelve some books"""
|
||||
shelve_actions = [
|
||||
(k, v)
|
||||
for k, v in request.POST.items()
|
||||
@ -99,10 +99,10 @@ class GetStartedBooks(View):
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class GetStartedUsers(View):
|
||||
""" find friends """
|
||||
"""find friends"""
|
||||
|
||||
def get(self, request):
|
||||
""" basic profile info """
|
||||
"""basic profile info"""
|
||||
query = request.GET.get("query")
|
||||
user_results = (
|
||||
models.User.viewer_aware_objects(request.user)
|
||||
@ -119,7 +119,7 @@ class GetStartedUsers(View):
|
||||
)
|
||||
|
||||
if user_results.count() < 5:
|
||||
suggested_users = None#get_suggested_users(request.user)
|
||||
suggested_users = None # get_suggested_users(request.user)
|
||||
|
||||
data = {
|
||||
"suggested_users": list(user_results) + list(suggested_users),
|
||||
|
@ -10,23 +10,23 @@ from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.status import create_generated_note
|
||||
from .helpers import get_user_from_username, object_visible_to_user
|
||||
from .helpers import get_user_from_username
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@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()
|
||||
if not goal and user != request.user:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if goal and not object_visible_to_user(request.user, goal):
|
||||
if goal and not goal.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
data = {
|
||||
@ -39,7 +39,7 @@ class Goal(View):
|
||||
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()
|
||||
@ -71,7 +71,7 @@ class Goal(View):
|
||||
@require_POST
|
||||
@login_required
|
||||
def hide_goal(request):
|
||||
""" don't keep bugging people to set a goal """
|
||||
"""don't keep bugging people to set a goal"""
|
||||
request.user.show_goal = False
|
||||
request.user.save(broadcast=False)
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
@ -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,44 +20,20 @@ def get_user_from_username(viewer, username):
|
||||
|
||||
|
||||
def is_api_request(request):
|
||||
""" check whether a request is asking for html or data """
|
||||
"""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 """
|
||||
"""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? """
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
# viewer can't see it if the object's owner blocked them
|
||||
if viewer in obj.user.blocks.all():
|
||||
return False
|
||||
|
||||
# you can see your own posts and any public or unlisted posts
|
||||
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():
|
||||
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():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
|
||||
""" filter objects that have "user" and "privacy" fields """
|
||||
"""filter objects that have "user" and "privacy" fields"""
|
||||
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
|
||||
# if there'd a deleted field, exclude deleted items
|
||||
try:
|
||||
@ -108,7 +84,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
|
||||
|
||||
|
||||
def handle_remote_webfinger(query):
|
||||
""" webfingerin' other servers """
|
||||
"""webfingerin' other servers"""
|
||||
user = None
|
||||
|
||||
# usernames could be @user@domain or user@domain
|
||||
@ -124,7 +100,7 @@ def handle_remote_webfinger(query):
|
||||
return None
|
||||
|
||||
try:
|
||||
user = models.User.objects.get(username=query)
|
||||
user = models.User.objects.get(username__iexact=query)
|
||||
except models.User.DoesNotExist:
|
||||
url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query)
|
||||
try:
|
||||
@ -138,13 +114,13 @@ def handle_remote_webfinger(query):
|
||||
user = activitypub.resolve_remote_id(
|
||||
link["href"], model=models.User
|
||||
)
|
||||
except KeyError:
|
||||
except (KeyError, activitypub.ActivitySerializerError):
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
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()
|
||||
@ -152,7 +128,7 @@ 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 = {
|
||||
@ -169,14 +145,14 @@ def handle_reading_status(user, shelf, book, privacy):
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_discover_books():
|
||||
""" list of books for the discover page """
|
||||
"""list of books for the discover page"""
|
||||
return list(
|
||||
set(
|
||||
models.Edition.objects.filter(
|
||||
|
@ -16,10 +16,10 @@ from bookwyrm.tasks import app
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Import(View):
|
||||
""" import view """
|
||||
"""import view"""
|
||||
|
||||
def get(self, request):
|
||||
""" load import page """
|
||||
"""load import page"""
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"import.html",
|
||||
@ -32,7 +32,7 @@ class Import(View):
|
||||
)
|
||||
|
||||
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"
|
||||
@ -66,10 +66,10 @@ class Import(View):
|
||||
|
||||
@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
|
||||
@ -84,7 +84,7 @@ class ImportStatus(View):
|
||||
)
|
||||
|
||||
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"):
|
||||
|
@ -1,9 +1,10 @@
|
||||
""" incoming activities """
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import urldefrag
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@ -12,15 +13,20 @@ import requests
|
||||
from bookwyrm import activitypub, models
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.signatures import Signature
|
||||
from bookwyrm.utils import regex
|
||||
|
||||
|
||||
@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"""
|
||||
# first check if this server is on our shitlist
|
||||
if is_blocked_user_agent(request):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
# make sure the user's inbox even exists
|
||||
if username:
|
||||
try:
|
||||
@ -34,6 +40,10 @@ class Inbox(View):
|
||||
except json.decoder.JSONDecodeError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# let's be extra sure we didn't block this domain
|
||||
if is_blocked_activity(activity_json):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
if (
|
||||
not "object" in activity_json
|
||||
or not "type" in activity_json
|
||||
@ -54,26 +64,47 @@ class Inbox(View):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
def is_blocked_user_agent(request):
|
||||
"""check if a request is from a blocked server based on user agent"""
|
||||
# check user agent
|
||||
user_agent = request.headers.get("User-Agent")
|
||||
if not user_agent:
|
||||
return False
|
||||
url = re.search(r"https?://{:s}/?".format(regex.domain), user_agent)
|
||||
if not url:
|
||||
return False
|
||||
url = url.group()
|
||||
return models.FederatedServer.is_blocked(url)
|
||||
|
||||
|
||||
def is_blocked_activity(activity_json):
|
||||
"""get the sender out of activity json and check if it's blocked"""
|
||||
actor = activity_json.get("actor")
|
||||
|
||||
# check if the user is banned/deleted
|
||||
existing = models.User.find_existing_by_remote_id(actor)
|
||||
if existing and existing.deleted:
|
||||
return True
|
||||
|
||||
if not actor:
|
||||
# well I guess it's not even a valid activity so who knows
|
||||
return False
|
||||
return models.FederatedServer.is_blocked(actor)
|
||||
|
||||
|
||||
@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)
|
||||
except activitypub.ActivitySerializerError:
|
||||
return
|
||||
activity = activitypub.parse(activity_json)
|
||||
|
||||
# cool that worked, now we should do the action described by the type
|
||||
# (create, update, delete, etc)
|
||||
try:
|
||||
activity.action()
|
||||
except activitypub.ActivitySerializerError:
|
||||
# this is raised if the activity is discarded
|
||||
return
|
||||
activity.action()
|
||||
|
||||
|
||||
def has_valid_signature(request, activity):
|
||||
""" verify incoming signature """
|
||||
"""verify incoming signature"""
|
||||
try:
|
||||
signature = Signature.parse(request)
|
||||
|
||||
|
@ -12,10 +12,10 @@ from bookwyrm import models
|
||||
# pylint: disable= no-self-use
|
||||
@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)
|
||||
@ -28,10 +28,10 @@ class Favorite(View):
|
||||
|
||||
@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)
|
||||
@ -45,10 +45,10 @@ class Unfavorite(View):
|
||||
|
||||
@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:
|
||||
@ -70,10 +70,10 @@ class Boost(View):
|
||||
|
||||
@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
|
||||
|
@ -26,15 +26,10 @@ from . import helpers
|
||||
name="dispatch",
|
||||
)
|
||||
class ManageInvites(View):
|
||||
""" create invites """
|
||||
"""create invites"""
|
||||
|
||||
def get(self, request):
|
||||
""" invite management page """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
"""invite management page"""
|
||||
paginated = Paginator(
|
||||
models.SiteInvite.objects.filter(user=request.user).order_by(
|
||||
"-created_date"
|
||||
@ -43,13 +38,13 @@ class ManageInvites(View):
|
||||
)
|
||||
|
||||
data = {
|
||||
"invites": paginated.page(page),
|
||||
"invites": paginated.get_page(request.GET.get("page")),
|
||||
"form": forms.CreateInviteForm(),
|
||||
}
|
||||
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,))
|
||||
@ -69,10 +64,10 @@ class ManageInvites(View):
|
||||
|
||||
|
||||
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("/")
|
||||
invite = get_object_or_404(models.SiteInvite, code=code)
|
||||
@ -88,16 +83,11 @@ class Invite(View):
|
||||
|
||||
|
||||
class ManageInviteRequests(View):
|
||||
""" grant invites like the benevolent lord you are """
|
||||
"""grant invites like the benevolent lord you are"""
|
||||
|
||||
def get(self, request):
|
||||
""" view a list of requests """
|
||||
"""view a list of requests"""
|
||||
ignored = request.GET.get("ignored", False)
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
sort = request.GET.get("sort")
|
||||
sort_fields = [
|
||||
"created_date",
|
||||
@ -136,13 +126,13 @@ class ManageInviteRequests(View):
|
||||
data = {
|
||||
"ignored": ignored,
|
||||
"count": paginated.count,
|
||||
"requests": paginated.page(page),
|
||||
"requests": paginated.get_page(request.GET.get("page")),
|
||||
"sort": sort,
|
||||
}
|
||||
return TemplateResponse(request, "settings/manage_invite_requests.html", data)
|
||||
|
||||
def post(self, request):
|
||||
""" send out an invite """
|
||||
"""send out an invite"""
|
||||
invite_request = get_object_or_404(
|
||||
models.InviteRequest, id=request.POST.get("invite-request")
|
||||
)
|
||||
@ -162,10 +152,10 @@ class ManageInviteRequests(View):
|
||||
|
||||
|
||||
class InviteRequest(View):
|
||||
""" prospective users sign up here """
|
||||
"""prospective users sign up here"""
|
||||
|
||||
def post(self, request):
|
||||
""" create a request """
|
||||
"""create a request"""
|
||||
form = forms.InviteRequestForm(request.POST)
|
||||
received = False
|
||||
if form.is_valid():
|
||||
@ -182,7 +172,7 @@ class InviteRequest(View):
|
||||
|
||||
@require_POST
|
||||
def ignore_invite_request(request):
|
||||
""" hide an invite request """
|
||||
"""hide an invite request"""
|
||||
invite_request = get_object_or_404(
|
||||
models.InviteRequest, id=request.POST.get("invite-request")
|
||||
)
|
||||
|
@ -13,10 +13,10 @@ 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):
|
||||
|
@ -9,18 +9,18 @@ from . import helpers
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
class About(View):
|
||||
""" create invites """
|
||||
"""create invites"""
|
||||
|
||||
def get(self, request):
|
||||
""" more information about the instance """
|
||||
"""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")
|
||||
@ -29,10 +29,10 @@ class Home(View):
|
||||
|
||||
|
||||
class Discover(View):
|
||||
""" preview of recently reviewed books """
|
||||
"""preview of recently reviewed books"""
|
||||
|
||||
def get(self, request):
|
||||
""" tiled book activity page """
|
||||
"""tiled book activity page"""
|
||||
data = {
|
||||
"register_form": forms.RegisterForm(),
|
||||
"request_form": forms.InviteRequestForm(),
|
||||
|
@ -1,9 +1,12 @@
|
||||
""" book list views"""
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpResponseNotFound, HttpResponseBadRequest
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import Avg, Count, Q, Max
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
@ -13,20 +16,16 @@ from django.views.decorators.http import require_POST
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from .helpers import is_api_request, object_visible_to_user, privacy_filter
|
||||
from .helpers import is_api_request, privacy_filter
|
||||
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 """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
"""display a book list"""
|
||||
# hide lists with no approved books
|
||||
lists = (
|
||||
models.List.objects.annotate(
|
||||
@ -35,7 +34,6 @@ class Lists(View):
|
||||
.filter(item_count__gt=0)
|
||||
.order_by("-updated_date")
|
||||
.distinct()
|
||||
.all()
|
||||
)
|
||||
|
||||
lists = privacy_filter(
|
||||
@ -44,7 +42,7 @@ class Lists(View):
|
||||
|
||||
paginated = Paginator(lists, 12)
|
||||
data = {
|
||||
"lists": paginated.page(page),
|
||||
"lists": paginated.get_page(request.GET.get("page")),
|
||||
"list_form": forms.ListForm(),
|
||||
"path": "/list",
|
||||
}
|
||||
@ -53,7 +51,7 @@ class Lists(View):
|
||||
@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")
|
||||
@ -63,23 +61,19 @@ class Lists(View):
|
||||
|
||||
|
||||
class UserLists(View):
|
||||
""" a user's book list page """
|
||||
"""a user's book list page"""
|
||||
|
||||
def get(self, request, username):
|
||||
""" display a book list """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
"""display a book list"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
lists = models.List.objects.filter(user=user).all()
|
||||
lists = models.List.objects.filter(user=user)
|
||||
lists = privacy_filter(request.user, lists)
|
||||
paginated = Paginator(lists, 12)
|
||||
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": request.user.id == user.id,
|
||||
"lists": paginated.page(page),
|
||||
"lists": paginated.get_page(request.GET.get("page")),
|
||||
"list_form": forms.ListForm(),
|
||||
"path": user.local_path + "/lists",
|
||||
}
|
||||
@ -87,12 +81,12 @@ class UserLists(View):
|
||||
|
||||
|
||||
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):
|
||||
if not book_list.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if is_api_request(request):
|
||||
@ -100,6 +94,45 @@ class List(View):
|
||||
|
||||
query = request.GET.get("q")
|
||||
suggestions = None
|
||||
|
||||
# sort_by shall be "order" unless a valid alternative is given
|
||||
sort_by = request.GET.get("sort_by", "order")
|
||||
if sort_by not in ("order", "title", "rating"):
|
||||
sort_by = "order"
|
||||
|
||||
# direction shall be "ascending" unless a valid alternative is given
|
||||
direction = request.GET.get("direction", "ascending")
|
||||
if direction not in ("ascending", "descending"):
|
||||
direction = "ascending"
|
||||
|
||||
internal_sort_by = {
|
||||
"order": "order",
|
||||
"title": "book__title",
|
||||
"rating": "average_rating",
|
||||
}
|
||||
directional_sort_by = internal_sort_by[sort_by]
|
||||
if direction == "descending":
|
||||
directional_sort_by = "-" + directional_sort_by
|
||||
|
||||
if sort_by == "order":
|
||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||
directional_sort_by
|
||||
)
|
||||
elif sort_by == "title":
|
||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||
directional_sort_by
|
||||
)
|
||||
elif sort_by == "rating":
|
||||
items = (
|
||||
book_list.listitem_set.annotate(
|
||||
average_rating=Avg(Coalesce("book__review__rating", 0))
|
||||
)
|
||||
.filter(approved=True)
|
||||
.order_by(directional_sort_by)
|
||||
)
|
||||
|
||||
paginated = Paginator(items, 12)
|
||||
|
||||
if query and request.user.is_authenticated:
|
||||
# search for books
|
||||
suggestions = connector_manager.local_search(query, raw=True)
|
||||
@ -119,18 +152,21 @@ class List(View):
|
||||
|
||||
data = {
|
||||
"list": book_list,
|
||||
"items": book_list.listitem_set.filter(approved=True),
|
||||
"items": paginated.get_page(request.GET.get("page")),
|
||||
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
||||
"suggested_books": suggestions,
|
||||
"list_form": forms.ListForm(instance=book_list),
|
||||
"query": query or "",
|
||||
"sort_form": forms.SortListForm(
|
||||
{"direction": direction, "sort_by": sort_by}
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "lists/list.html", data)
|
||||
|
||||
@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():
|
||||
@ -140,11 +176,11 @@ class List(View):
|
||||
|
||||
|
||||
class Curate(View):
|
||||
""" approve or discard list suggestsions """
|
||||
"""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
|
||||
@ -160,42 +196,65 @@ class Curate(View):
|
||||
@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"
|
||||
if approved:
|
||||
# update the book and set it to be the last in the order of approved books,
|
||||
# before any pending books
|
||||
suggestion.approved = True
|
||||
order_max = (
|
||||
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
||||
"order__max"
|
||||
]
|
||||
or 0
|
||||
) + 1
|
||||
suggestion.order = order_max
|
||||
increment_order_in_reverse(book_list.id, order_max)
|
||||
suggestion.save()
|
||||
else:
|
||||
suggestion.delete()
|
||||
deleted_order = suggestion.order
|
||||
suggestion.delete(broadcast=False)
|
||||
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
||||
return redirect("list-curate", book_list.id)
|
||||
|
||||
|
||||
@require_POST
|
||||
def add_book(request):
|
||||
""" put a book on a list """
|
||||
"""put a book on a list"""
|
||||
book_list = get_object_or_404(models.List, id=request.POST.get("list"))
|
||||
if not object_visible_to_user(request.user, book_list):
|
||||
if not book_list.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
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":
|
||||
# go ahead and add it
|
||||
# add the book at the latest order of approved books, before pending books
|
||||
order_max = (
|
||||
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
||||
"order__max"
|
||||
]
|
||||
) or 0
|
||||
increment_order_in_reverse(book_list.id, order_max + 1)
|
||||
models.ListItem.objects.create(
|
||||
book=book,
|
||||
book_list=book_list,
|
||||
user=request.user,
|
||||
order=order_max + 1,
|
||||
)
|
||||
elif book_list.curation == "curated":
|
||||
# make a pending entry
|
||||
# make a pending entry at the end of the list
|
||||
order_max = (
|
||||
book_list.listitem_set.aggregate(Max("order"))["order__max"]
|
||||
) or 0
|
||||
models.ListItem.objects.create(
|
||||
approved=False,
|
||||
book=book,
|
||||
book_list=book_list,
|
||||
user=request.user,
|
||||
order=order_max + 1,
|
||||
)
|
||||
else:
|
||||
# you can't add to this list, what were you THINKING
|
||||
@ -209,12 +268,113 @@ def add_book(request):
|
||||
|
||||
@require_POST
|
||||
def remove_book(request, list_id):
|
||||
""" 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"))
|
||||
"""remove a book from a list"""
|
||||
with transaction.atomic():
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
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()
|
||||
if not book_list.user == request.user and not item.user == request.user:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
item.delete()
|
||||
deleted_order = item.order
|
||||
item.delete()
|
||||
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
||||
return redirect("list", list_id)
|
||||
|
||||
|
||||
@require_POST
|
||||
def set_book_position(request, list_item_id):
|
||||
"""
|
||||
Action for when the list user manually specifies a list position, takes
|
||||
special care with the unique ordering per list.
|
||||
"""
|
||||
with transaction.atomic():
|
||||
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
||||
try:
|
||||
int_position = int(request.POST.get("position"))
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest(
|
||||
"bad value for position. should be an integer"
|
||||
)
|
||||
|
||||
if int_position < 1:
|
||||
return HttpResponseBadRequest("position cannot be less than 1")
|
||||
|
||||
book_list = list_item.book_list
|
||||
|
||||
# the max position to which a book may be set is the highest order for
|
||||
# books which are approved
|
||||
order_max = book_list.listitem_set.filter(approved=True).aggregate(
|
||||
Max("order")
|
||||
)["order__max"]
|
||||
|
||||
if int_position > order_max:
|
||||
int_position = order_max
|
||||
|
||||
if request.user not in (book_list.user, list_item.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
original_order = list_item.order
|
||||
if original_order == int_position:
|
||||
return HttpResponse(status=204)
|
||||
if original_order > int_position:
|
||||
list_item.order = -1
|
||||
list_item.save()
|
||||
increment_order_in_reverse(book_list.id, int_position, original_order)
|
||||
else:
|
||||
list_item.order = -1
|
||||
list_item.save()
|
||||
decrement_order(book_list.id, original_order, int_position)
|
||||
|
||||
list_item.order = int_position
|
||||
list_item.save()
|
||||
|
||||
return redirect("list", book_list.id)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def increment_order_in_reverse(
|
||||
book_list_id: int, start: int, end: Optional[int] = None
|
||||
):
|
||||
"""increase the order nu,ber for every item in a list"""
|
||||
try:
|
||||
book_list = models.List.objects.get(id=book_list_id)
|
||||
except models.List.DoesNotExist:
|
||||
return
|
||||
items = book_list.listitem_set.filter(order__gte=start)
|
||||
if end is not None:
|
||||
items = items.filter(order__lt=end)
|
||||
items = items.order_by("-order")
|
||||
for item in items:
|
||||
item.order += 1
|
||||
item.save()
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def decrement_order(book_list_id, start, end):
|
||||
"""decrement the order value for every item in a list"""
|
||||
try:
|
||||
book_list = models.List.objects.get(id=book_list_id)
|
||||
except models.List.DoesNotExist:
|
||||
return
|
||||
items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by(
|
||||
"order"
|
||||
)
|
||||
for item in items:
|
||||
item.order -= 1
|
||||
item.save()
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def normalize_book_list_ordering(book_list_id, start=0, add_offset=0):
|
||||
"""gives each book in a list the proper sequential order number"""
|
||||
try:
|
||||
book_list = models.List.objects.get(id=book_list_id)
|
||||
except models.List.DoesNotExist:
|
||||
return
|
||||
items = book_list.listitem_set.filter(order__gt=start).order_by("order")
|
||||
for i, item in enumerate(items, start):
|
||||
effective_order = i + add_offset
|
||||
if item.order != effective_order:
|
||||
item.order = effective_order
|
||||
item.save()
|
||||
|
@ -9,10 +9,10 @@ from django.views import View
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Notifications(View):
|
||||
""" notifications view """
|
||||
"""notifications view"""
|
||||
|
||||
def get(self, request):
|
||||
""" people are interacting with you, get hyped """
|
||||
"""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 = {
|
||||
@ -23,6 +23,6 @@ class Notifications(View):
|
||||
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")
|
||||
|
@ -9,10 +9,10 @@ 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")
|
||||
if filter_type not in models.status_models:
|
||||
|
@ -14,17 +14,17 @@ 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",
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
""" create a password reset token """
|
||||
"""create a password reset token"""
|
||||
email = request.POST.get("email")
|
||||
try:
|
||||
user = models.User.objects.get(email=email)
|
||||
@ -43,10 +43,10 @@ class PasswordResetRequest(View):
|
||||
|
||||
|
||||
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("/")
|
||||
try:
|
||||
@ -59,7 +59,7 @@ class PasswordReset(View):
|
||||
return TemplateResponse(request, "password_reset.html", {"code": code})
|
||||
|
||||
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)
|
||||
except models.PasswordReset.DoesNotExist:
|
||||
@ -84,15 +84,15 @@ class PasswordReset(View):
|
||||
|
||||
@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 """
|
||||
"""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 """
|
||||
"""allow a user to change their password"""
|
||||
new_password = request.POST.get("password")
|
||||
confirm_password = request.POST.get("confirm-password")
|
||||
|
||||
|
@ -18,7 +18,7 @@ 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)
|
||||
reading_shelf = models.Shelf.objects.filter(
|
||||
identifier=models.Shelf.READING, user=request.user
|
||||
@ -60,7 +60,7 @@ def start_reading(request, book_id):
|
||||
@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)
|
||||
finished_read_shelf = models.Shelf.objects.filter(
|
||||
identifier=models.Shelf.READ_FINISHED, user=request.user
|
||||
@ -101,7 +101,7 @@ def finish_reading(request, book_id):
|
||||
@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()
|
||||
@ -121,7 +121,7 @@ def edit_readthrough(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_readthrough(request):
|
||||
""" remove a readthrough """
|
||||
"""remove a readthrough"""
|
||||
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
|
||||
|
||||
# don't let people edit other people's data
|
||||
@ -135,7 +135,7 @@ def delete_readthrough(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def create_readthrough(request):
|
||||
""" can't use the form because the dates are too finnicky """
|
||||
"""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:
|
||||
@ -145,13 +145,14 @@ def create_readthrough(request):
|
||||
|
||||
|
||||
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
||||
"""ensures that data is stored consistently in the UTC timezone"""
|
||||
user_tz = dateutil.tz.gettz(user.preferred_timezone)
|
||||
start_date = dateutil.parser.parse(date_str, ignoretz=True)
|
||||
return start_date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
|
||||
|
||||
|
||||
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")
|
||||
if not read_id:
|
||||
@ -208,7 +209,7 @@ def update_readthrough(request, book=None, create=True):
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_progressupdate(request):
|
||||
""" remove a progress update """
|
||||
"""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
|
||||
|
@ -20,17 +20,19 @@ from bookwyrm import forms, models
|
||||
name="dispatch",
|
||||
)
|
||||
class Reports(View):
|
||||
""" list of reports """
|
||||
"""list of reports"""
|
||||
|
||||
def get(self, request):
|
||||
""" view current reports """
|
||||
"""view current reports"""
|
||||
filters = {}
|
||||
|
||||
resolved = request.GET.get("resolved") == "true"
|
||||
server = request.GET.get("server")
|
||||
if server:
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
filters["user__federated_server"] = server
|
||||
filters["user__federated_server__server_name"] = server
|
||||
username = request.GET.get("username")
|
||||
if username:
|
||||
filters["user__username__icontains"] = username
|
||||
filters["resolved"] = resolved
|
||||
data = {
|
||||
"resolved": resolved,
|
||||
@ -50,17 +52,17 @@ class Reports(View):
|
||||
name="dispatch",
|
||||
)
|
||||
class Report(View):
|
||||
""" view a specific report """
|
||||
"""view a specific report"""
|
||||
|
||||
def get(self, request, report_id):
|
||||
""" load a report """
|
||||
"""load a report"""
|
||||
data = {
|
||||
"report": get_object_or_404(models.Report, id=report_id),
|
||||
}
|
||||
return TemplateResponse(request, "moderation/report.html", data)
|
||||
|
||||
def post(self, request, report_id):
|
||||
""" comment on a report """
|
||||
"""comment on a report"""
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
models.ReportComment.objects.create(
|
||||
user=request.user,
|
||||
@ -72,18 +74,19 @@ class Report(View):
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_user")
|
||||
def deactivate_user(_, report_id):
|
||||
""" mark an account as inactive """
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
report.user.is_active = not report.user.is_active
|
||||
report.user.save()
|
||||
return redirect("settings-report", report.id)
|
||||
def suspend_user(_, user_id):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
user.is_active = not user.is_active
|
||||
# this isn't a full deletion, so we don't want to tell the world
|
||||
user.save(broadcast=False)
|
||||
return redirect("settings-user", user.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm_moderate_post")
|
||||
def resolve_report(_, report_id):
|
||||
""" mark a report as (un)resolved """
|
||||
"""mark a report as (un)resolved"""
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
report.resolved = not report.resolved
|
||||
report.save()
|
||||
@ -95,11 +98,10 @@ def resolve_report(_, report_id):
|
||||
@login_required
|
||||
@require_POST
|
||||
def make_report(request):
|
||||
""" a user reports something """
|
||||
"""a user reports something"""
|
||||
form = forms.ReportForm(request.POST)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
raise ValueError(form.errors)
|
||||
|
||||
form.save()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
@ -5,25 +5,25 @@ from .helpers import get_user_from_username, privacy_filter
|
||||
|
||||
# pylint: disable=no-self-use, unused-argument
|
||||
class RssFeed(Feed):
|
||||
""" serialize user's posts in rss feed """
|
||||
"""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 """
|
||||
"""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 privacy_filter(
|
||||
obj,
|
||||
obj.status_set.select_subclasses(),
|
||||
@ -31,5 +31,5 @@ class RssFeed(Feed):
|
||||
)
|
||||
|
||||
def item_link(self, item):
|
||||
""" link to the status """
|
||||
"""link to the status"""
|
||||
return item.local_path
|
||||
|
@ -16,10 +16,10 @@ 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 """
|
||||
"""that search bar up top"""
|
||||
query = request.GET.get("q")
|
||||
min_confidence = request.GET.get("min_confidence", 0.1)
|
||||
|
||||
@ -34,7 +34,7 @@ class Search(View):
|
||||
if query and re.match(regex.full_username, query):
|
||||
handle_remote_webfinger(query)
|
||||
|
||||
# do a user search
|
||||
# do a user search
|
||||
user_results = (
|
||||
models.User.viewer_aware_objects(request.user)
|
||||
.annotate(
|
||||
|
@ -16,25 +16,20 @@ from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request, get_edition, get_user_from_username
|
||||
from .helpers import handle_reading_status, privacy_filter, object_visible_to_user
|
||||
from .helpers import handle_reading_status, privacy_filter
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
class Shelf(View):
|
||||
""" shelf page """
|
||||
"""shelf page"""
|
||||
|
||||
def get(self, request, username, shelf_identifier=None):
|
||||
""" display a shelf """
|
||||
"""display a shelf"""
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
shelves = privacy_filter(request.user, user.shelf_set)
|
||||
|
||||
# get the shelf and make sure the logged in user should be able to see it
|
||||
@ -43,7 +38,7 @@ class Shelf(View):
|
||||
shelf = user.shelf_set.get(identifier=shelf_identifier)
|
||||
except models.Shelf.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
if not object_visible_to_user(request.user, shelf):
|
||||
if not shelf.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
# this is a constructed "all books" view, with a fake "shelf" obj
|
||||
else:
|
||||
@ -61,7 +56,7 @@ class Shelf(View):
|
||||
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
||||
|
||||
paginated = Paginator(
|
||||
shelf.books.order_by("-updated_date").all(),
|
||||
shelf.books.order_by("-updated_date"),
|
||||
PAGE_LENGTH,
|
||||
)
|
||||
|
||||
@ -70,7 +65,7 @@ class Shelf(View):
|
||||
"is_self": is_self,
|
||||
"shelves": shelves.all(),
|
||||
"shelf": shelf,
|
||||
"books": paginated.page(page),
|
||||
"books": paginated.get_page(request.GET.get("page")),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "user/shelf.html", data)
|
||||
@ -78,7 +73,7 @@ class Shelf(View):
|
||||
@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:
|
||||
@ -99,7 +94,7 @@ class Shelf(View):
|
||||
@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", "/"))
|
||||
@ -111,7 +106,7 @@ def create_shelf(request):
|
||||
@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()
|
||||
@ -123,7 +118,7 @@ def delete_shelf(request, shelf_id):
|
||||
@login_required
|
||||
@require_POST
|
||||
def shelve(request):
|
||||
""" put a book on a user's shelf """
|
||||
"""put a book on a user's shelf"""
|
||||
book = get_edition(request.POST.get("book"))
|
||||
|
||||
desired_shelf = models.Shelf.objects.filter(
|
||||
@ -182,7 +177,7 @@ def shelve(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def unshelve(request):
|
||||
""" put a on a user's 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"])
|
||||
|
||||
@ -192,6 +187,6 @@ def unshelve(request):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_unshelve(book, shelf):
|
||||
""" unshelve a book """
|
||||
"""unshelve a book"""
|
||||
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
|
||||
row.delete()
|
||||
|
@ -15,16 +15,16 @@ from bookwyrm import emailing, forms, models
|
||||
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)
|
||||
|
||||
def post(self, request):
|
||||
""" edit the site settings """
|
||||
"""edit the site settings"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
form = forms.SiteForm(request.POST, request.FILES, instance=site)
|
||||
if not form.is_valid():
|
||||
@ -38,7 +38,7 @@ class Site(View):
|
||||
@login_required
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
def email_preview(request):
|
||||
""" for development, renders and example email template """
|
||||
"""for development, renders and example email template"""
|
||||
template = request.GET.get("email")
|
||||
data = emailing.email_data()
|
||||
data["subject_path"] = "email/{}/subject.html".format(template)
|
||||
|
@ -19,16 +19,16 @@ from .reading import edit_readthrough
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class CreateStatus(View):
|
||||
""" the view for *posting* """
|
||||
"""the view for *posting*"""
|
||||
|
||||
def get(self, request):
|
||||
""" compose view (used for delete-and-redraft """
|
||||
"""compose view (used for delete-and-redraft"""
|
||||
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
|
||||
data = {"book": book}
|
||||
return TemplateResponse(request, "compose.html", data)
|
||||
|
||||
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:
|
||||
@ -80,10 +80,10 @@ class CreateStatus(View):
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
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
|
||||
@ -97,10 +97,10 @@ class DeleteStatus(View):
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class DeleteAndRedraft(View):
|
||||
""" delete a status but let the user re-create it """
|
||||
"""delete a status but let the user re-create it"""
|
||||
|
||||
def post(self, request, status_id):
|
||||
""" delete and tombstone a status """
|
||||
"""delete and tombstone a status"""
|
||||
status = get_object_or_404(
|
||||
models.Status.objects.select_subclasses(), id=status_id
|
||||
)
|
||||
@ -130,7 +130,7 @@ class DeleteAndRedraft(View):
|
||||
|
||||
|
||||
def find_mentions(content):
|
||||
""" detect @mentions in raw status content """
|
||||
"""detect @mentions in raw status content"""
|
||||
if not content:
|
||||
return
|
||||
for match in re.finditer(regex.strict_username, content):
|
||||
@ -148,7 +148,7 @@ 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'\g<1><a href="\g<2>">\g<3></a>',
|
||||
@ -157,7 +157,7 @@ def format_links(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,73 +0,0 @@
|
||||
""" 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
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from .helpers import is_api_request
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
class Tag(View):
|
||||
""" tag page """
|
||||
|
||||
def get(self, request, tag_id):
|
||||
""" 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))
|
||||
|
||||
books = models.Edition.objects.filter(
|
||||
usertag__tag__identifier=tag_id
|
||||
).distinct()
|
||||
data = {
|
||||
"books": books,
|
||||
"tag": tag_obj,
|
||||
}
|
||||
return TemplateResponse(request, "tag.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AddTag(View):
|
||||
""" add a tag to a book """
|
||||
|
||||
def post(self, request):
|
||||
""" tag a book """
|
||||
# I'm not using a form here because sometimes "name" is sent as a hidden
|
||||
# field which doesn't validate
|
||||
name = request.POST.get("name")
|
||||
book_id = request.POST.get("book")
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
tag_obj, _ = models.Tag.objects.get_or_create(
|
||||
name=name,
|
||||
)
|
||||
models.UserTag.objects.get_or_create(
|
||||
user=request.user,
|
||||
book=book,
|
||||
tag=tag_obj,
|
||||
)
|
||||
|
||||
return redirect("/book/%s" % book_id)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class RemoveTag(View):
|
||||
""" remove a user's tag from a book """
|
||||
|
||||
def post(self, request):
|
||||
""" 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 = 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
|
||||
)
|
||||
user_tag.delete()
|
||||
|
||||
return redirect("/book/%s" % book_id)
|
@ -7,7 +7,7 @@ from bookwyrm import activitystreams
|
||||
|
||||
@login_required
|
||||
def get_notification_count(request):
|
||||
""" any notifications waiting? """
|
||||
"""any notifications waiting?"""
|
||||
return JsonResponse(
|
||||
{
|
||||
"count": request.user.notification_set.filter(read=False).count(),
|
||||
@ -17,7 +17,7 @@ def get_notification_count(request):
|
||||
|
||||
@login_required
|
||||
def get_unread_status_count(request, stream="home"):
|
||||
""" any unread statuses for this feed? """
|
||||
"""any unread statuses for this feed?"""
|
||||
stream = activitystreams.streams.get(stream)
|
||||
if not stream:
|
||||
return JsonResponse({})
|
||||
|
@ -17,15 +17,15 @@ from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import get_user_from_username, is_api_request
|
||||
from .helpers import is_blocked, privacy_filter, object_visible_to_user
|
||||
from .helpers import is_blocked, privacy_filter
|
||||
|
||||
|
||||
# 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,11 +40,6 @@ class User(View):
|
||||
return ActivitypubResponse(user.to_activity())
|
||||
# otherwise we're at a UI view
|
||||
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
shelf_preview = []
|
||||
|
||||
# only show other shelves that should be visible
|
||||
@ -80,14 +75,14 @@ class User(View):
|
||||
goal = models.AnnualGoal.objects.filter(
|
||||
user=user, year=timezone.now().year
|
||||
).first()
|
||||
if not object_visible_to_user(request.user, goal):
|
||||
if goal and not goal.visible_to_user(request.user):
|
||||
goal = None
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": is_self,
|
||||
"shelves": shelf_preview,
|
||||
"shelf_count": shelves.count(),
|
||||
"activities": paginated.page(page),
|
||||
"activities": paginated.get_page(request.GET.get("page", 1)),
|
||||
"goal": goal,
|
||||
}
|
||||
|
||||
@ -95,10 +90,10 @@ class User(View):
|
||||
|
||||
|
||||
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:
|
||||
@ -120,10 +115,10 @@ class Followers(View):
|
||||
|
||||
|
||||
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:
|
||||
@ -146,10 +141,10 @@ class Following(View):
|
||||
|
||||
@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,
|
||||
@ -157,7 +152,7 @@ class EditUser(View):
|
||||
return TemplateResponse(request, "preferences/edit_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
""" les get fancy with images """
|
||||
"""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}
|
||||
@ -169,7 +164,7 @@ class EditUser(View):
|
||||
|
||||
|
||||
def save_user_form(form):
|
||||
""" special handling for the user form """
|
||||
"""special handling for the user form"""
|
||||
user = form.save(commit=False)
|
||||
|
||||
if "avatar" in form.files:
|
||||
@ -190,7 +185,7 @@ def save_user_form(form):
|
||||
|
||||
|
||||
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 = (
|
||||
|
@ -6,7 +6,7 @@ from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
||||
|
||||
@ -16,23 +16,22 @@ from bookwyrm.settings import PAGE_LENGTH
|
||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class UserAdmin(View):
|
||||
""" admin view of users on this server """
|
||||
class UserAdminList(View):
|
||||
"""admin view of users on this server"""
|
||||
|
||||
def get(self, request):
|
||||
""" list of users """
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
"""list of users"""
|
||||
filters = {}
|
||||
server = request.GET.get("server")
|
||||
if server:
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
server = models.FederatedServer.objects.filter(server_name=server).first()
|
||||
filters["federated_server"] = server
|
||||
filters["federated_server__isnull"] = False
|
||||
username = request.GET.get("username")
|
||||
if username:
|
||||
filters["username__icontains"] = username
|
||||
|
||||
users = models.User.objects.filter(**filters).all()
|
||||
users = models.User.objects.filter(**filters)
|
||||
|
||||
sort = request.GET.get("sort", "-created_date")
|
||||
sort_fields = [
|
||||
@ -46,5 +45,33 @@ class UserAdmin(View):
|
||||
users = users.order_by(sort)
|
||||
|
||||
paginated = Paginator(users, PAGE_LENGTH)
|
||||
data = {"users": paginated.page(page), "sort": sort, "server": server}
|
||||
return TemplateResponse(request, "settings/user_admin.html", data)
|
||||
data = {
|
||||
"users": paginated.get_page(request.GET.get("page")),
|
||||
"sort": sort,
|
||||
"server": server,
|
||||
}
|
||||
return TemplateResponse(request, "user_admin/user_admin.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class UserAdmin(View):
|
||||
"""moderate an individual user"""
|
||||
|
||||
def get(self, request, user):
|
||||
"""user view"""
|
||||
user = get_object_or_404(models.User, id=user)
|
||||
data = {"user": user, "group_form": forms.UserGroupForm()}
|
||||
return TemplateResponse(request, "user_admin/user.html", data)
|
||||
|
||||
def post(self, request, user):
|
||||
"""update user group"""
|
||||
user = get_object_or_404(models.User, id=user)
|
||||
form = forms.UserGroupForm(request.POST, instance=user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
data = {"user": user, "group_form": form}
|
||||
return TemplateResponse(request, "user_admin/user.html", data)
|
||||
|
@ -13,7 +13,7 @@ from bookwyrm.settings import DOMAIN, VERSION
|
||||
|
||||
@require_GET
|
||||
def webfinger(request):
|
||||
""" allow other servers to ask about a user """
|
||||
"""allow other servers to ask about a user"""
|
||||
resource = request.GET.get("resource")
|
||||
if not resource or not resource.startswith("acct:"):
|
||||
return HttpResponseNotFound()
|
||||
@ -40,7 +40,7 @@ def webfinger(request):
|
||||
|
||||
@require_GET
|
||||
def nodeinfo_pointer(_):
|
||||
""" direct servers to nodeinfo """
|
||||
"""direct servers to nodeinfo"""
|
||||
return JsonResponse(
|
||||
{
|
||||
"links": [
|
||||
@ -55,7 +55,7 @@ def nodeinfo_pointer(_):
|
||||
|
||||
@require_GET
|
||||
def nodeinfo(_):
|
||||
""" basic info about the server """
|
||||
"""basic info about the server"""
|
||||
status_count = models.Status.objects.filter(user__local=True).count()
|
||||
user_count = models.User.objects.filter(local=True).count()
|
||||
|
||||
@ -90,7 +90,7 @@ def nodeinfo(_):
|
||||
|
||||
@require_GET
|
||||
def instance_info(_):
|
||||
""" let's talk about your cool unique instance """
|
||||
"""let's talk about your cool unique instance"""
|
||||
user_count = models.User.objects.filter(local=True).count()
|
||||
status_count = models.Status.objects.filter(user__local=True).count()
|
||||
|
||||
@ -116,12 +116,12 @@ def instance_info(_):
|
||||
|
||||
@require_GET
|
||||
def peers(_):
|
||||
""" list of federated servers this instance connects with """
|
||||
"""list of federated servers this instance connects with"""
|
||||
names = models.FederatedServer.objects.values_list("server_name", flat=True)
|
||||
return JsonResponse(list(names), safe=False)
|
||||
|
||||
|
||||
@require_GET
|
||||
def host_meta(request):
|
||||
""" meta of the host """
|
||||
"""meta of the host"""
|
||||
return TemplateResponse(request, "host_meta.xml", {"DOMAIN": DOMAIN})
|
||||
|
Reference in New Issue
Block a user