1
0
Fork 0

Merge branch 'main' into opensearch

This commit is contained in:
Mouse Reeve 2021-09-27 18:58:28 -07:00
commit 15fc31bf77
143 changed files with 3967 additions and 1784 deletions

View file

@ -1,40 +1,14 @@
""" make sure all our nice views are available """
from .announcements import Announcements, Announcement, delete_announcement
from .author import Author, EditAuthor
from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook
from .books import upload_cover, add_description, resolve_book
from .directory import Directory
from .discover import Discover
from .edit_user import EditUser, DeleteUser
from .editions import Editions, switch_edition
from .email_blocklist import EmailBlocklist
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
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
from .goal import Goal, hide_goal
from .import_data import Import, ImportStatus
from .inbox import Inbox
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite, InviteRequest
from .invite import ManageInviteRequests, ignore_invite_request
from .isbn import Isbn
from .landing import About, Home, Landing
from .list import Lists, SavedLists, List, Curate, UserLists
from .list import save_list, unsave_list
from .list import delete_list
from .login import Login, Logout
from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough
from .reading import delete_readthrough, delete_progressupdate
from .reading import ReadingStatus
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
from .reports import (
from .admin.announcements import Announcements, Announcement, delete_announcement
from .admin.dashboard import Dashboard
from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist
from .admin.federation import block_server, unblock_server
from .admin.email_blocklist import EmailBlocklist
from .admin.ip_blocklist import IPBlocklist
from .admin.invite import ManageInvites, Invite, InviteRequest
from .admin.invite import ManageInviteRequests, ignore_invite_request
from .admin.reports import (
Report,
Reports,
make_report,
@ -43,15 +17,42 @@ from .reports import (
unsuspend_user,
moderator_delete_user,
)
from .admin.site import Site
from .admin.user_admin import UserAdmin, UserAdminList
from .author import Author, EditAuthor
from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook
from .books import upload_cover, add_description, resolve_book
from .directory import Directory
from .discover import Discover
from .edit_user import EditUser, DeleteUser
from .editions import Editions, switch_edition
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request
from .get_started import GetStartedBooks, GetStartedProfile, GetStartedUsers
from .goal import Goal, hide_goal
from .import_data import Import, ImportStatus
from .inbox import Inbox
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .isbn import Isbn
from .landing import About, Home, Landing
from .list import Lists, SavedLists, List, Curate, UserLists
from .list import save_list, unsave_list, delete_list
from .login import Login, Logout
from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough
from .reading import delete_readthrough, delete_progressupdate
from .reading import ReadingStatus
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search
from .shelf import Shelf
from .shelf import create_shelf, delete_shelf
from .shelf import shelve, unshelve
from .site import Site
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .updates import get_notification_count, get_unread_status_count
from .user import User, Followers, Following, hide_suggestions
from .user_admin import UserAdmin, UserAdminList
from .wellknown import *

View file

View file

@ -31,6 +31,7 @@ class Announcements(View):
"end_date",
"active",
]
# pylint: disable=consider-using-f-string
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
announcements = announcements.order_by(sort)
data = {

View file

@ -0,0 +1,88 @@
""" instance overview """
from datetime import timedelta
from dateutil.parser import parse
from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import Q
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_user", raise_exception=True),
name="dispatch",
)
class Dashboard(View):
"""admin overview"""
def get(self, request):
"""list of users"""
interval = int(request.GET.get("days", 1))
now = timezone.now()
user_queryset = models.User.objects.filter(local=True)
user_stats = {"labels": [], "total": [], "active": []}
status_queryset = models.Status.objects.filter(user__local=True, deleted=False)
status_stats = {"labels": [], "total": []}
start = request.GET.get("start")
if start:
start = timezone.make_aware(parse(start))
else:
start = now - timedelta(days=6 * interval)
end = request.GET.get("end")
end = timezone.make_aware(parse(end)) if end else now
start = start.replace(hour=0, minute=0, second=0)
interval_start = start
interval_end = interval_start + timedelta(days=interval)
while interval_start <= end:
print(interval_start, interval_end)
interval_queryset = user_queryset.filter(
Q(is_active=True) | Q(deactivation_date__gt=interval_end),
created_date__lte=interval_end,
)
user_stats["total"].append(interval_queryset.filter().count())
user_stats["active"].append(
interval_queryset.filter(
last_active_date__gt=interval_end - timedelta(days=31),
).count()
)
user_stats["labels"].append(interval_start.strftime("%b %d"))
status_stats["total"].append(
status_queryset.filter(
created_date__gt=interval_start,
created_date__lte=interval_end,
).count()
)
status_stats["labels"].append(interval_start.strftime("%b %d"))
interval_start = interval_end
interval_end += timedelta(days=interval)
data = {
"start": start.strftime("%Y-%m-%d"),
"end": end.strftime("%Y-%m-%d"),
"interval": interval,
"users": user_queryset.filter(is_active=True).count(),
"active_users": user_queryset.filter(
is_active=True, last_active_date__gte=now - timedelta(days=31)
).count(),
"statuses": status_queryset.count(),
"works": models.Work.objects.count(),
"reports": models.Report.objects.filter(resolved=False).count(),
"invite_requests": models.InviteRequest.objects.filter(
ignored=False, invite_sent=False
).count(),
"user_stats": user_stats,
"status_stats": status_stats,
}
return TemplateResponse(request, "settings/dashboard.html", data)

View file

@ -1,4 +1,4 @@
""" moderation via flagged posts and users """
""" Manage email blocklist"""
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
@ -14,7 +14,7 @@ from bookwyrm import forms, models
name="dispatch",
)
class EmailBlocklist(View):
"""Block users by email address"""
"""Block registration by email address"""
def get(self, request):
"""view and compose blocks"""

View file

@ -22,20 +22,25 @@ from bookwyrm.settings import PAGE_LENGTH
class Federation(View):
"""what servers do we federate with"""
def get(self, request):
def get(self, request, status="federated"):
"""list of servers"""
servers = models.FederatedServer.objects
servers = models.FederatedServer.objects.filter(status=status)
sort = request.GET.get("sort")
sort_fields = ["created_date", "application_type", "server_name"]
# pylint: disable=consider-using-f-string
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
sort = "created_date"
sort = "-created_date"
servers = servers.order_by(sort)
paginated = Paginator(servers, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data = {
"servers": paginated.get_page(request.GET.get("page")),
"servers": page,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
"sort": sort,
"form": forms.ServerForm(),
}

View file

@ -16,7 +16,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import emailing, forms, models
from bookwyrm.settings import PAGE_LENGTH
from . import helpers
from bookwyrm.views import helpers
# pylint: disable= no-self-use
@ -51,7 +51,7 @@ class ManageInvites(View):
"""creates an invite database entry"""
form = forms.CreateInviteForm(request.POST)
if not form.is_valid():
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
return HttpResponseBadRequest(f"ERRORS: {form.errors}")
invite = form.save(commit=False)
invite.user = request.user
@ -98,6 +98,7 @@ class ManageInviteRequests(View):
"invite__times_used",
"invite__invitees__created_date",
]
# pylint: disable=consider-using-f-string
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
sort = "-created_date"
@ -149,6 +150,7 @@ class ManageInviteRequests(View):
)
invite_request.save()
emailing.invite_email(invite_request)
# pylint: disable=consider-using-f-string
return redirect(
"{:s}?{:s}".format(
reverse("settings-invite-requests"), urlencode(request.GET.dict())

View file

@ -0,0 +1,49 @@
""" Manage IP blocklist """
from django.contrib.auth.decorators import login_required, permission_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 forms, models
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_user", raise_exception=True),
name="dispatch",
)
class IPBlocklist(View):
"""Block registration by ip address"""
def get(self, request):
"""view and compose blocks"""
data = {
"addresses": models.IPBlocklist.objects.all(),
"form": forms.IPBlocklistForm(),
}
return TemplateResponse(request, "settings/ip_blocklist.html", data)
def post(self, request, block_id=None):
"""create a new ip address block"""
if block_id:
return self.delete(request, block_id)
form = forms.IPBlocklistForm(request.POST)
data = {
"addresses": models.IPBlocklist.objects.all(),
"form": form,
}
if not form.is_valid():
return TemplateResponse(request, "settings/ip_blocklist.html", data)
form.save()
data["form"] = forms.IPBlocklistForm()
return TemplateResponse(request, "settings/ip_blocklist.html", data)
# pylint: disable=unused-argument
def delete(self, request, domain_id):
"""remove a domain block"""
domain = get_object_or_404(models.IPBlocklist, id=domain_id)
domain.delete()
return redirect("settings-ip-blocks")

View file

@ -41,9 +41,9 @@ def email_preview(request):
"""for development, renders and example email template"""
template = request.GET.get("email")
data = emailing.email_data()
data["subject_path"] = "email/{}/subject.html".format(template)
data["html_content_path"] = "email/{}/html_content.html".format(template)
data["text_content_path"] = "email/{}/text_content.html".format(template)
data["subject_path"] = f"email/{template}/subject.html"
data["html_content_path"] = f"email/{template}/html_content.html"
data["text_content_path"] = f"email/{template}/text_content.html"
data["reset_link"] = "https://example.com/link"
data["invite_link"] = "https://example.com/link"
data["confirmation_link"] = "https://example.com/link"

View file

@ -47,6 +47,7 @@ class UserAdminList(View):
"federated_server__server_name",
"is_active",
]
# pylint: disable=consider-using-f-string
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
users = users.order_by(sort)

View file

@ -55,4 +55,4 @@ class EditAuthor(View):
return TemplateResponse(request, "author/edit_author.html", data)
author = form.save()
return redirect("/author/%s" % author.id)
return redirect(f"/author/{author.id}")

View file

@ -24,7 +24,7 @@ class Block(View):
models.UserBlocks.objects.create(
user_subject=request.user, user_object=to_block
)
return redirect("/preferences/block")
return redirect("prefs-block")
@require_POST
@ -40,4 +40,4 @@ def unblock(request, user_id):
except models.UserBlocks.DoesNotExist:
return HttpResponseNotFound()
block.delete()
return redirect("/preferences/block")
return redirect("prefs-block")

View file

@ -8,7 +8,7 @@ from django.core.files.base import ContentFile
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.http import HttpResponseBadRequest, Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
@ -30,25 +30,31 @@ class Book(View):
def get(self, request, book_id, user_statuses=False):
"""info about a book"""
user_statuses = user_statuses if request.user.is_authenticated else False
try:
book = models.Book.objects.select_subclasses().get(id=book_id)
except models.Book.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
book = get_object_or_404(
models.Book.objects.select_subclasses(), id=book_id
)
return ActivitypubResponse(book.to_activity())
if isinstance(book, models.Work):
book = book.default_edition
user_statuses = user_statuses if request.user.is_authenticated else False
# it's safe to use this OR because edition and work and subclasses of the same
# table, so they never have clashing IDs
book = (
models.Edition.viewer_aware_objects(request.user)
.filter(Q(id=book_id) | Q(parent_work__id=book_id))
.order_by("-edition_rank")
.select_related("parent_work")
.prefetch_related("authors")
.first()
)
if not book or not book.parent_work:
return HttpResponseNotFound()
raise Http404
work = book.parent_work
# all reviews for the book
# all reviews for all editions of the book
reviews = privacy_filter(
request.user, models.Review.objects.filter(book__in=work.editions.all())
request.user, models.Review.objects.filter(book__parent_work__editions=book)
)
# the reviews to show
@ -174,7 +180,7 @@ class EditBook(View):
# check if this is an edition of an existing work
author_text = book.author_text if book else add_author
data["book_matches"] = connector_manager.local_search(
"%s %s" % (form.cleaned_data.get("title"), author_text),
f'{form.cleaned_data.get("title")} {author_text}',
min_confidence=0.5,
raw=True,
)[:5]
@ -212,7 +218,7 @@ class EditBook(View):
if image:
book.cover.save(*image, save=False)
book.save()
return redirect("/book/%s" % book.id)
return redirect(f"/book/{book.id}")
@method_decorator(login_required, name="dispatch")
@ -238,14 +244,14 @@ class ConfirmEditBook(View):
# get or create author as needed
for i in range(int(request.POST.get("author-match-count", 0))):
match = request.POST.get("author_match-%d" % i)
match = request.POST.get(f"author_match-{i}")
if not match:
return HttpResponseBadRequest()
try:
# if it's an int, it's an ID
match = int(match)
author = get_object_or_404(
models.Author, id=request.POST["author_match-%d" % i]
models.Author, id=request.POST[f"author_match-{i}"]
)
except ValueError:
# otherwise it's a name
@ -267,7 +273,7 @@ class ConfirmEditBook(View):
for author_id in request.POST.getlist("remove_authors"):
book.authors.remove(author_id)
return redirect("/book/%s" % book.id)
return redirect(f"/book/{book.id}")
@login_required
@ -283,7 +289,7 @@ def upload_cover(request, book_id):
if image:
book.cover.save(*image)
return redirect("{:s}?cover_error=True".format(book.local_path))
return redirect(f"{book.local_path}?cover_error=True")
form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid() or not form.files.get("cover"):

View file

@ -34,9 +34,9 @@ class EditUser(View):
data = {"form": form, "user": request.user}
return TemplateResponse(request, "preferences/edit_user.html", data)
user = save_user_form(form)
save_user_form(form)
return redirect(user.local_path)
return redirect("user-feed", request.user.localname)
# pylint: disable=no-self-use
@ -79,7 +79,7 @@ def save_user_form(form):
# set the name to a hash
extension = form.files["avatar"].name.split(".")[-1]
filename = "%s.%s" % (uuid4(), extension)
filename = f"{uuid4()}.{extension}"
user.avatar.save(filename, image, save=False)
user.save()
return user

View file

@ -96,4 +96,4 @@ def switch_edition(request):
readthrough.book = new_edition
readthrough.save()
return redirect("/book/%d" % new_edition.id)
return redirect(f"/book/{new_edition.id}")

View file

@ -42,7 +42,7 @@ class Feed(View):
"tab": tab,
"streams": STREAMS,
"goal_form": forms.GoalForm(),
"path": "/%s" % tab["key"],
"path": f"/{tab['key']}",
},
}
return TemplateResponse(request, "feed/feed.html", data)
@ -168,9 +168,11 @@ def get_suggested_books(user, max_books=5):
shelf_preview = {
"name": shelf.name,
"identifier": shelf.identifier,
"books": shelf.books.order_by("shelfbook").prefetch_related("authors")[
:limit
],
"books": models.Edition.viewer_aware_objects(user)
.filter(
shelfbook__shelf=shelf,
)
.prefetch_related("authors")[:limit],
}
suggested_books.append(shelf_preview)
book_count += len(shelf_preview["books"])

View file

@ -86,4 +86,4 @@ def delete_follow_request(request):
return HttpResponseBadRequest()
follow_request.delete()
return redirect("/user/%s" % request.user.localname)
return redirect(f"/user/{request.user.localname}")

View file

@ -77,7 +77,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
elif "followers" in privacy_levels:
queryset = queryset.exclude(
~Q( # user isn't following and it isn't their own status
Q(user__in=viewer.following.all()) | Q(user=viewer)
Q(user__followers=viewer) | Q(user=viewer)
),
privacy="followers", # and the status is followers only
)
@ -113,7 +113,7 @@ def handle_remote_webfinger(query):
try:
user = models.User.objects.get(username__iexact=query)
except models.User.DoesNotExist:
url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query)
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
try:
data = get_data(url)
except (ConnectorException, HTTPError):

View file

@ -68,7 +68,7 @@ class Import(View):
importer.start_import(job)
return redirect("/import/%d" % job.id)
return redirect(f"/import/{job.id}")
return HttpResponseBadRequest()
@ -112,4 +112,4 @@ class ImportStatus(View):
items,
)
importer.start_import(job)
return redirect("/import/%d" % job.id)
return redirect(f"/import/{job.id}")

View file

@ -71,7 +71,7 @@ def is_blocked_user_agent(request):
user_agent = request.headers.get("User-Agent")
if not user_agent:
return False
url = re.search(r"https?://{:s}/?".format(regex.DOMAIN), user_agent)
url = re.search(rf"https?://{regex.DOMAIN}/?", user_agent)
if not url:
return False
url = url.group()

View file

@ -36,6 +36,8 @@ class Lists(View):
item_count=Count("listitem", filter=Q(listitem__approved=True))
)
.filter(item_count__gt=0)
.select_related("user")
.prefetch_related("listitem_set")
.order_by("-updated_date")
.distinct()
)
@ -322,7 +324,7 @@ def add_book(request):
path = reverse("list", args=[book_list.id])
params = request.GET.copy()
params["updated"] = True
return redirect("{:s}?{:s}".format(path, urlencode(params)))
return redirect(f"{path}?{urlencode(params)}")
@require_POST
@ -396,7 +398,7 @@ def set_book_position(request, list_item_id):
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"""
"""increase the order number for every item in a list"""
try:
book_list = models.List.objects.get(id=book_list_id)
except models.List.DoesNotExist:

View file

@ -3,7 +3,6 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
@ -46,7 +45,7 @@ class Login(View):
except models.User.DoesNotExist: # maybe it's a full username?
username = localname
else:
username = "%s@%s" % (localname, DOMAIN)
username = f"{localname}@{DOMAIN}"
password = login_form.data["password"]
# perform authentication
@ -54,8 +53,7 @@ class Login(View):
if user is not None:
# successful login
login(request, user)
user.last_active_date = timezone.now()
user.save(broadcast=False, update_fields=["last_active_date"])
user.update_active_date()
if request.POST.get("first_login"):
return redirect("get-started-profile")
return redirect(request.GET.get("next", "/"))

View file

@ -29,4 +29,4 @@ class Notifications(View):
def post(self, request):
"""permanently delete notification for user"""
request.user.notification_set.filter(read=True).delete()
return redirect("/notifications")
return redirect("notifications")

View file

@ -27,7 +27,9 @@ class PasswordResetRequest(View):
"""create a password reset token"""
email = request.POST.get("email")
try:
user = models.User.objects.get(email=email, email__isnull=False)
user = models.User.viewer_aware_objects(request.user).get(
email=email, email__isnull=False
)
except models.User.DoesNotExist:
data = {"error": _("No user with that email address was found.")}
return TemplateResponse(request, "password_reset_request.html", data)
@ -38,7 +40,7 @@ class PasswordResetRequest(View):
# create a new reset code
code = models.PasswordReset.objects.create(user=user)
password_reset_email(code)
data = {"message": _("A password reset link sent to %s" % email)}
data = {"message": _(f"A password reset link sent to {email}")}
return TemplateResponse(request, "password_reset_request.html", data)
@ -97,9 +99,9 @@ class ChangePassword(View):
confirm_password = request.POST.get("confirm-password")
if new_password != confirm_password:
return redirect("preferences/password")
return redirect("prefs-password")
request.user.set_password(new_password)
request.user.save(broadcast=False, update_fields=["password"])
login(request, request.user)
return redirect(request.user.local_path)
return redirect("user-feed", request.user.localname)

View file

@ -5,6 +5,7 @@ import dateutil.tz
from dateutil.parser import ParserError
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
@ -35,7 +36,7 @@ class ReadingStatus(View):
return TemplateResponse(request, f"reading_progress/{template}", {"book": book})
def post(self, request, status, book_id):
"""desire a book"""
"""Change the state of a book by shelving it and adding reading dates"""
identifier = {
"want": models.Shelf.TO_READ,
"start": models.Shelf.READING,
@ -48,18 +49,21 @@ class ReadingStatus(View):
identifier=identifier, user=request.user
).first()
book = get_edition(book_id)
current_status_shelfbook = (
models.ShelfBook.objects.select_related("shelf")
.filter(
shelf__identifier__in=models.Shelf.READ_STATUS_IDENTIFIERS,
user=request.user,
book=book,
)
.first()
book = (
models.Edition.viewer_aware_objects(request.user)
.prefetch_related("shelfbook_set__shelf")
.get(id=book_id)
)
# gets the first shelf that indicates a reading status, or None
shelves = [
s
for s in book.current_shelves
if s.shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS
]
current_status_shelfbook = shelves[0] if shelves else None
# checking the referer prevents redirecting back to the modal page
referer = request.headers.get("Referer", "/")
referer = "/" if "reading-status" in referer else referer
if current_status_shelfbook is not None:
@ -72,11 +76,13 @@ class ReadingStatus(View):
book=book, shelf=desired_shelf, user=request.user
)
if desired_shelf.identifier != models.Shelf.TO_READ:
# update or create a readthrough
readthrough = update_readthrough(request, book=book)
if readthrough:
readthrough.save()
update_readthrough_on_shelve(
request.user,
book,
desired_shelf.identifier,
start_date=request.POST.get("start_date"),
finish_date=request.POST.get("finish_date"),
)
# post about it (if you want)
if request.POST.get("post-status"):
@ -97,17 +103,67 @@ class ReadingStatus(View):
return redirect(referer)
@transaction.atomic
def update_readthrough_on_shelve(
user, annotated_book, status, start_date=None, finish_date=None
):
"""update the current readthrough for a book when it is re-shelved"""
# there *should* only be one of current active readthrough, but it's a list
active_readthrough = next(iter(annotated_book.active_readthroughs), None)
# deactivate all existing active readthroughs
for readthrough in annotated_book.active_readthroughs:
readthrough.is_active = False
readthrough.save()
# if the state is want-to-read, deactivating existing readthroughs is all we need
if status == models.Shelf.TO_READ:
return
# if we're starting a book, we need a fresh clean active readthrough
if status == models.Shelf.READING or not active_readthrough:
active_readthrough = models.ReadThrough.objects.create(
user=user, book=annotated_book
)
# santiize and set dates
active_readthrough.start_date = load_date_in_user_tz_as_utc(start_date, user)
# if the finish date is set, the readthrough will be automatically set as inactive
active_readthrough.finish_date = load_date_in_user_tz_as_utc(finish_date, user)
active_readthrough.save()
@login_required
@require_POST
def edit_readthrough(request):
"""can't use the form because the dates are too finnicky"""
readthrough = update_readthrough(request, create=False)
if not readthrough:
return HttpResponseNotFound()
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.start_date = load_date_in_user_tz_as_utc(
request.POST.get("start_date"), request.user
)
readthrough.finish_date = load_date_in_user_tz_as_utc(
request.POST.get("finish_date"), request.user
)
progress = request.POST.get("progress")
try:
progress = int(progress)
readthrough.progress = progress
except (ValueError, TypeError):
pass
progress_mode = request.POST.get("progress_mode")
try:
progress_mode = models.ProgressMode(progress_mode)
readthrough.progress_mode = progress_mode
except ValueError:
pass
readthrough.save()
# record the progress update individually
@ -136,73 +192,32 @@ def delete_readthrough(request):
def create_readthrough(request):
"""can't use the form because the dates are too finnicky"""
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
readthrough = update_readthrough(request, create=True, book=book)
if not readthrough:
return redirect(book.local_path)
readthrough.save()
return redirect(request.headers.get("Referer", "/"))
start_date = load_date_in_user_tz_as_utc(
request.POST.get("start_date"), request.user
)
finish_date = load_date_in_user_tz_as_utc(
request.POST.get("finish_date"), request.user
)
models.ReadThrough.objects.create(
user=request.user,
book=book,
start_date=start_date,
finish_date=finish_date,
)
return redirect("book", book.id)
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"""
try:
read_id = request.POST.get("id")
if not read_id:
raise models.ReadThrough.DoesNotExist
readthrough = models.ReadThrough.objects.get(id=read_id)
except models.ReadThrough.DoesNotExist:
if not create or not book:
return None
readthrough = models.ReadThrough(
user=request.user,
book=book,
)
start_date = request.POST.get("start_date")
if start_date:
try:
readthrough.start_date = load_date_in_user_tz_as_utc(
start_date, request.user
)
except ParserError:
pass
finish_date = request.POST.get("finish_date")
if finish_date:
try:
readthrough.finish_date = load_date_in_user_tz_as_utc(
finish_date, request.user
)
except ParserError:
pass
progress = request.POST.get("progress")
if progress:
try:
progress = int(progress)
readthrough.progress = progress
except ValueError:
pass
progress_mode = request.POST.get("progress_mode")
if progress_mode:
try:
progress_mode = models.ProgressMode(progress_mode)
readthrough.progress_mode = progress_mode
except ValueError:
pass
if not readthrough.start_date and not readthrough.finish_date:
if not date_str:
return None
user_tz = dateutil.tz.gettz(user.preferred_timezone)
date = dateutil.parser.parse(date_str, ignoretz=True)
try:
return date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
except ParserError:
return None
return readthrough
@login_required

View file

@ -16,6 +16,10 @@ from bookwyrm.settings import DOMAIN
class Register(View):
"""register a user"""
def get(self, request): # pylint: disable=unused-argument
"""whether or not you're logged in, just go to the home view"""
return redirect("/")
@sensitive_variables("password")
@method_decorator(sensitive_post_parameters("password"))
def post(self, request):
@ -64,7 +68,7 @@ class Register(View):
return TemplateResponse(request, "invite.html", data)
return TemplateResponse(request, "login.html", data)
username = "%s@%s" % (localname, DOMAIN)
username = f"{localname}@{DOMAIN}"
user = models.User.objects.create_user(
username,
email,

View file

@ -63,7 +63,7 @@ class Search(View):
data["results"] = paginated
data["remote"] = search_remote
return TemplateResponse(request, "search/{:s}.html".format(search_type), data)
return TemplateResponse(request, f"search/{search_type}.html", data)
def book_search(query, _, min_confidence, search_remote=False):

View file

@ -31,9 +31,9 @@ class Shelf(View):
is_self = user == request.user
if is_self:
shelves = user.shelf_set
shelves = user.shelf_set.all()
else:
shelves = privacy_filter(request.user, user.shelf_set)
shelves = privacy_filter(request.user, user.shelf_set).all()
# get the shelf and make sure the logged in user should be able to see it
if shelf_identifier:
@ -49,10 +49,14 @@ class Shelf(View):
FakeShelf = namedtuple(
"Shelf", ("identifier", "name", "user", "books", "privacy")
)
books = models.Edition.objects.filter(
# privacy is ensured because the shelves are already filtered above
shelfbook__shelf__in=shelves.all()
).distinct()
books = (
models.Edition.viewer_aware_objects(request.user)
.filter(
# privacy is ensured because the shelves are already filtered above
shelfbook__shelf__in=shelves
)
.distinct()
)
shelf = FakeShelf("all", _("All books"), user, books, "public")
if is_api_request(request):
@ -82,7 +86,7 @@ class Shelf(View):
data = {
"user": user,
"is_self": is_self,
"shelves": shelves.all(),
"shelves": shelves,
"shelf": shelf,
"books": page,
"page_range": paginated.get_elided_page_range(

View file

@ -5,7 +5,7 @@ from urllib.parse import urlparse
from django.contrib.auth.decorators import login_required
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from django.http import HttpResponse, HttpResponseBadRequest
from django.http import HttpResponse, HttpResponseBadRequest, Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
@ -36,7 +36,7 @@ class CreateStatus(View):
status_type = status_type[0].upper() + status_type[1:]
try:
form = getattr(forms, "%sForm" % status_type)(request.POST)
form = getattr(forms, f"{status_type}Form")(request.POST)
except AttributeError:
return HttpResponseBadRequest()
if not form.is_valid():
@ -58,8 +58,8 @@ class CreateStatus(View):
# turn the mention into a link
content = re.sub(
r"%s([^@]|$)" % mention_text,
r'<a href="%s">%s</a>\g<1>' % (mention_user.remote_id, mention_text),
rf"{mention_text}([^@]|$)",
rf'<a href="{mention_user.remote_id}">{mention_text}</a>\g<1>',
content,
)
# add reply parent to mentions
@ -79,7 +79,10 @@ class CreateStatus(View):
status.save(created=True)
# update a readthorugh, if needed
edit_readthrough(request)
try:
edit_readthrough(request)
except Http404:
pass
if is_api_request(request):
return HttpResponse()
@ -182,7 +185,7 @@ def format_links(content):
if url.fragment != "":
link += "#" + url.fragment
formatted_content += '<a href="%s">%s</a>' % (potential_link, link)
formatted_content += f'<a href="{potential_link}">{link}</a>'
except (ValidationError, UnicodeError):
formatted_content += potential_link

View file

@ -59,8 +59,18 @@ class User(View):
request.user,
user.status_set.select_subclasses(),
)
.select_related("reply_parent")
.prefetch_related("mention_books", "mention_users")
.select_related(
"user",
"reply_parent",
"review__book",
"comment__book",
"quotation__book",
)
.prefetch_related(
"mention_books",
"mention_users",
"attachments",
)
)
paginated = Paginator(activities, PAGE_LENGTH)

View file

@ -26,7 +26,7 @@ def webfinger(request):
return JsonResponse(
{
"subject": "acct:%s" % (user.username),
"subject": f"acct:{user.username}",
"links": [
{
"rel": "self",
@ -46,7 +46,7 @@ def nodeinfo_pointer(_):
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": "https://%s/nodeinfo/2.0" % DOMAIN,
"href": f"https://{DOMAIN}/nodeinfo/2.0",
}
]
}
@ -56,17 +56,17 @@ def nodeinfo_pointer(_):
@require_GET
def nodeinfo(_):
"""basic info about the server"""
status_count = models.Status.objects.filter(user__local=True).count()
user_count = models.User.objects.filter(local=True).count()
status_count = models.Status.objects.filter(user__local=True, deleted=False).count()
user_count = models.User.objects.filter(is_active=True, local=True).count()
month_ago = timezone.now() - relativedelta(months=1)
last_month_count = models.User.objects.filter(
local=True, last_active_date__gt=month_ago
is_active=True, local=True, last_active_date__gt=month_ago
).count()
six_months_ago = timezone.now() - relativedelta(months=6)
six_month_count = models.User.objects.filter(
local=True, last_active_date__gt=six_months_ago
is_active=True, local=True, last_active_date__gt=six_months_ago
).count()
site = models.SiteSettings.get()
@ -91,11 +91,11 @@ def nodeinfo(_):
@require_GET
def instance_info(_):
"""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()
user_count = models.User.objects.filter(is_active=True, local=True).count()
status_count = models.Status.objects.filter(user__local=True, deleted=False).count()
site = models.SiteSettings.get()
logo_path = site.logo_small or "images/logo-small.png"
logo_path = site.logo or "images/logo.png"
logo = f"{MEDIA_FULL_URL}{logo_path}"
return JsonResponse(
{
@ -111,7 +111,7 @@ def instance_info(_):
"thumbnail": logo,
"languages": ["en"],
"registrations": site.allow_registration,
"approval_required": False,
"approval_required": site.allow_registration and site.allow_invite_requests,
"email": site.admin_email,
}
)
@ -120,7 +120,9 @@ def instance_info(_):
@require_GET
def peers(_):
"""list of federated servers this instance connects with"""
names = models.FederatedServer.objects.values_list("server_name", flat=True)
names = models.FederatedServer.objects.filter(status="federated").values_list(
"server_name", flat=True
)
return JsonResponse(list(names), safe=False)