Merge branch 'main' into opensearch
This commit is contained in:
commit
15fc31bf77
143 changed files with 3967 additions and 1784 deletions
|
@ -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 *
|
||||
|
|
0
bookwyrm/views/admin/__init__.py
Normal file
0
bookwyrm/views/admin/__init__.py
Normal 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 = {
|
88
bookwyrm/views/admin/dashboard.py
Normal file
88
bookwyrm/views/admin/dashboard.py
Normal 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)
|
|
@ -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"""
|
|
@ -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(),
|
||||
}
|
|
@ -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())
|
49
bookwyrm/views/admin/ip_blocklist.py
Normal file
49
bookwyrm/views/admin/ip_blocklist.py
Normal 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")
|
|
@ -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"
|
|
@ -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)
|
||||
|
|
@ -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}")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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", "/"))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue