Merge branch 'main' into suggestions-redis

This commit is contained in:
Mouse Reeve
2021-04-26 10:40:19 -07:00
278 changed files with 9210 additions and 5868 deletions

View File

@ -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 *

View File

@ -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("/")

View File

@ -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)

View File

@ -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(

View File

@ -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(

View File

@ -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")

View File

@ -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)

View File

@ -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 = {

View File

@ -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)

View File

@ -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),

View File

@ -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", "/"))

View File

@ -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(

View File

@ -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"):

View File

@ -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)

View File

@ -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

View File

@ -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")
)

View File

@ -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):

View File

@ -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(),

View File

@ -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()

View File

@ -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")

View File

@ -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:

View File

@ -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")

View File

@ -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

View File

@ -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", "/"))

View File

@ -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

View File

@ -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(

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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({})

View File

@ -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 = (

View File

@ -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)

View File

@ -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})