Merge branch 'main' into suggestions-redis

This commit is contained in:
Mouse Reeve
2021-05-20 20:25:15 -07:00
committed by GitHub
200 changed files with 11270 additions and 6135 deletions

View File

@ -1,4 +1,5 @@
""" make sure all our nice views are available """
from .announcements import Announcements, Announcement, delete_announcement
from .authentication import Login, Register, Logout
from .author import Author, EditAuthor
from .block import Block, unblock

View File

@ -0,0 +1,97 @@
""" make announcements """
from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
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
from bookwyrm.settings import PAGE_LENGTH
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
class Announcements(View):
"""tell everyone"""
def get(self, request):
"""view and create announcements"""
announcements = models.Announcement.objects
sort = request.GET.get("sort", "-created_date")
sort_fields = [
"created_date",
"preview",
"start_date",
"end_date",
"active",
]
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
announcements = announcements.order_by(sort)
data = {
"announcements": Paginator(announcements, PAGE_LENGTH).get_page(
request.GET.get("page")
),
"form": forms.AnnouncementForm(),
"sort": sort,
}
return TemplateResponse(request, "settings/announcements.html", data)
def post(self, request):
"""edit the site settings"""
form = forms.AnnouncementForm(request.POST)
if form.is_valid():
form.save()
# reset the create form
form = forms.AnnouncementForm()
data = {
"announcements": Paginator(
models.Announcement.objects.all(), PAGE_LENGTH
).get_page(request.GET.get("page")),
"form": form,
}
return TemplateResponse(request, "settings/announcements.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
class Announcement(View):
"""edit an announcement"""
def get(self, request, announcement_id):
"""view announcement"""
announcement = get_object_or_404(models.Announcement, id=announcement_id)
data = {
"announcement": announcement,
"form": forms.AnnouncementForm(instance=announcement),
}
return TemplateResponse(request, "settings/announcement.html", data)
def post(self, request, announcement_id):
"""edit announcement"""
announcement = get_object_or_404(models.Announcement, id=announcement_id)
form = forms.AnnouncementForm(request.POST, instance=announcement)
if form.is_valid():
announcement = form.save()
data = {
"announcement": announcement,
"form": form,
}
return TemplateResponse(request, "settings/announcement.html", data)
@login_required
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
def delete_announcement(_, announcement_id):
"""delete announcement"""
announcement = get_object_or_404(models.Announcement, id=announcement_id)
announcement.delete()
return redirect("settings-announcements")

View File

@ -27,9 +27,9 @@ class Author(View):
).distinct()
data = {
"author": author,
"books": [b.get_default_edition() for b in books],
"books": [b.default_edition for b in books],
}
return TemplateResponse(request, "author.html", data)
return TemplateResponse(request, "author/author.html", data)
@method_decorator(login_required, name="dispatch")
@ -43,7 +43,7 @@ class EditAuthor(View):
"""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)
return TemplateResponse(request, "author/edit_author.html", data)
def post(self, request, author_id):
"""edit a author cool"""
@ -52,7 +52,7 @@ class EditAuthor(View):
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
if not form.is_valid():
data = {"author": author, "form": form}
return TemplateResponse(request, "edit_author.html", data)
return TemplateResponse(request, "author/edit_author.html", data)
author = form.save()
return redirect("/author/%s" % author.id)

View File

@ -30,6 +30,7 @@ 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:
@ -39,7 +40,7 @@ class Book(View):
return ActivitypubResponse(book.to_activity())
if isinstance(book, models.Work):
book = book.get_default_edition()
book = book.default_edition
if not book or not book.parent_work:
return HttpResponseNotFound()
@ -51,9 +52,9 @@ class Book(View):
)
# the reviews to show
if user_statuses and request.user.is_authenticated:
if user_statuses:
if user_statuses == "review":
queryset = book.review_set
queryset = book.review_set.select_subclasses()
elif user_statuses == "comment":
queryset = book.comment_set
else:
@ -67,7 +68,9 @@ class Book(View):
"book": book,
"statuses": paginated.get_page(request.GET.get("page")),
"review_count": reviews.count(),
"ratings": reviews.filter(Q(content__isnull=True) | Q(content="")),
"ratings": reviews.filter(Q(content__isnull=True) | Q(content=""))
if not user_statuses
else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": privacy_filter(
request.user, book.list_set.filter(listitem__approved=True)
@ -156,7 +159,6 @@ class EditBook(View):
),
}
)
print(data["author_matches"])
# we're creating a new book
if not book:
@ -317,7 +319,10 @@ def upload_cover(request, book_id):
def set_cover_from_url(url):
"""load it from a url"""
image_file = get_image(url)
try:
image_file = get_image(url)
except: # pylint: disable=bare-except
return None
if not image_file:
return None
image_name = str(uuid4()) + "." + url.split(".")[-1]

View File

@ -2,7 +2,7 @@
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpResponseNotFound
from django.http import HttpResponseNotFound, Http404
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
@ -63,7 +63,7 @@ class DirectMessage(View):
if username:
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
except Http404:
pass
if user:
queryset = queryset.filter(Q(user=user) | Q(mention_users=user))
@ -95,7 +95,7 @@ class Status(View):
status = models.Status.objects.select_subclasses().get(
id=status_id, deleted=False
)
except (ValueError, models.Status.DoesNotExist, models.User.DoesNotExist):
except (ValueError, models.Status.DoesNotExist):
return HttpResponseNotFound()
# the url should have the poster's username in it

View File

@ -14,10 +14,7 @@ from .helpers import get_user_from_username
def follow(request):
"""follow another user, here or abroad"""
username = request.POST["user"]
try:
to_follow = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
to_follow = get_user_from_username(request.user, username)
try:
models.UserFollowRequest.objects.create(
@ -35,10 +32,7 @@ def follow(request):
def unfollow(request):
"""unfollow a user"""
username = request.POST["user"]
try:
to_unfollow = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
to_unfollow = get_user_from_username(request.user, username)
try:
models.UserFollows.objects.get(
@ -63,10 +57,7 @@ def unfollow(request):
def accept_follow_request(request):
"""a user accepts a follow request"""
username = request.POST["user"]
try:
requester = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
requester = get_user_from_username(request.user, username)
try:
follow_request = models.UserFollowRequest.objects.get(
@ -85,10 +76,7 @@ def accept_follow_request(request):
def delete_follow_request(request):
"""a user rejects a follow request"""
username = request.POST["user"]
try:
requester = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
requester = get_user_from_username(request.user, username)
try:
follow_request = models.UserFollowRequest.objects.get(

View File

@ -119,9 +119,10 @@ class GetStartedUsers(View):
)
if user_results.count() < 5:
suggested_users = None # get_suggested_users(request.user)
suggested_users = [] # TODO: get_suggested_users(request.user)
user_results = list(user_results) + list(suggested_users))
data = {
"suggested_users": list(user_results) + list(suggested_users),
"suggested_users": user_results,
}
return TemplateResponse(request, "get_started/users.html", data)
return TemplateResponse(request, "get_started/users.html", data)

View File

@ -3,6 +3,7 @@ import re
from requests import HTTPError
from django.core.exceptions import FieldError
from django.db.models import Count, Max, Q
from django.http import Http404
from bookwyrm import activitypub, models
from bookwyrm.connectors import ConnectorException, get_data
@ -12,11 +13,17 @@ from bookwyrm.utils import regex
def get_user_from_username(viewer, username):
"""helper function to resolve a localname or a username to a user"""
# raises DoesNotExist if user is now found
# raises 404 if the user isn't found
try:
return models.User.viewer_aware_objects(viewer).get(localname=username)
except models.User.DoesNotExist:
pass
# if the localname didn't match, try the username
try:
return models.User.viewer_aware_objects(viewer).get(username=username)
except models.User.DoesNotExist:
raise Http404()
def is_api_request(request):
@ -123,7 +130,7 @@ def get_edition(book_id):
"""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()
book = book.default_edition
return book
@ -165,4 +172,4 @@ def get_discover_books():
.annotate(Max("review__published_date"))
.order_by("-review__published_date__max")[:6]
)
)
)

View File

@ -7,10 +7,16 @@ from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from bookwyrm import forms, models
from bookwyrm.importers import Importer, LibrarythingImporter, GoodreadsImporter
from bookwyrm.importers import (
Importer,
LibrarythingImporter,
GoodreadsImporter,
StorygraphImporter,
)
from bookwyrm.tasks import app
# pylint: disable= no-self-use
@ -42,6 +48,8 @@ class Import(View):
importer = None
if source == "LibraryThing":
importer = LibrarythingImporter()
elif source == "Storygraph":
importer = StorygraphImporter()
else:
# Default : GoodReads
importer = GoodreadsImporter()
@ -55,8 +63,8 @@ class Import(View):
include_reviews,
privacy,
)
except (UnicodeDecodeError, ValueError):
return HttpResponseBadRequest("Not a valid csv file")
except (UnicodeDecodeError, ValueError, KeyError):
return HttpResponseBadRequest(_("Not a valid csv file"))
importer.start_import(job)

View File

@ -1,14 +1,16 @@
""" book list views"""
from typing import Optional
from urllib.parse import urlencode
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db import IntegrityError, transaction
from django.db.models import Avg, Count, Q, Max
from django.db.models import Avg, Count, DecimalField, 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.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
@ -16,6 +18,7 @@ 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 bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, privacy_filter
from .helpers import get_user_from_username
@ -105,37 +108,33 @@ class List(View):
if direction not in ("ascending", "descending"):
direction = "ascending"
internal_sort_by = {
directional_sort_by = {
"order": "order",
"title": "book__title",
"rating": "average_rating",
}
directional_sort_by = internal_sort_by[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))
items = book_list.listitem_set
if sort_by == "rating":
items = items.annotate(
average_rating=Avg(
Coalesce("book__review__rating", 0.0),
output_field=DecimalField(),
)
.filter(approved=True)
.order_by(directional_sort_by)
)
items = items.filter(approved=True).order_by(directional_sort_by)
paginated = Paginator(items, 12)
paginated = Paginator(items, PAGE_LENGTH)
if query and request.user.is_authenticated:
# search for books
suggestions = connector_manager.local_search(query, raw=True)
suggestions = connector_manager.local_search(
query,
raw=True,
filters=[~Q(parent_work__editions__in=book_list.books.all())],
)
elif request.user.is_authenticated:
# just suggest whatever books are nearby
suggestions = request.user.shelfbook_set.filter(
@ -150,9 +149,13 @@ class List(View):
).order_by("-updated_date")
][: 5 - len(suggestions)]
page = paginated.get_page(request.GET.get("page"))
data = {
"list": book_list,
"items": paginated.get_page(request.GET.get("page")),
"items": page,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
"pending_count": book_list.listitem_set.filter(approved=False).count(),
"suggested_books": suggestions,
"list_form": forms.ListForm(instance=book_list),
@ -263,7 +266,10 @@ def add_book(request):
# if the book is already on the list, don't flip out
pass
return redirect("list", book_list.id)
path = reverse("list", args=[book_list.id])
params = request.GET.copy()
params["updated"] = True
return redirect("{:s}?{:s}".format(path, urlencode(params)))
@require_POST

View File

@ -11,12 +11,16 @@ from django.views import View
class Notifications(View):
"""notifications view"""
def get(self, request):
def get(self, request, notification_type=None):
"""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)]
if notification_type == "mentions":
notifications = notifications.filter(
notification_type__in=["REPLY", "MENTION", "TAG"]
)
unread = [n.id for n in notifications.filter(read=False)[:50]]
data = {
"notifications": notifications,
"notifications": notifications[:50],
"unread": unread,
}
notifications.update(read=True)

View File

@ -27,7 +27,7 @@ class PasswordResetRequest(View):
"""create a password reset token"""
email = request.POST.get("email")
try:
user = models.User.objects.get(email=email)
user = models.User.objects.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)

View File

@ -7,8 +7,8 @@ from .helpers import get_user_from_username, privacy_filter
class RssFeed(Feed):
"""serialize user's posts in rss feed"""
description_template = "snippets/rss_content.html"
title_template = "snippets/rss_title.html"
description_template = "rss/content.html"
title_template = "rss/title.html"
def get_object(self, request, username):
"""the user who's posts get serialized"""

View File

@ -2,6 +2,7 @@
import re
from django.contrib.postgres.search import TrigramSimilarity
from django.core.paginator import Paginator
from django.db.models.functions import Greatest
from django.http import JsonResponse
from django.template.response import TemplateResponse
@ -9,6 +10,7 @@ from django.views import View
from bookwyrm import models
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.utils import regex
from .helpers import is_api_request, privacy_filter
from .helpers import handle_remote_webfinger
@ -22,6 +24,10 @@ class Search(View):
"""that search bar up top"""
query = request.GET.get("q")
min_confidence = request.GET.get("min_confidence", 0.1)
search_type = request.GET.get("type")
search_remote = (
request.GET.get("remote", False) and request.user.is_authenticated
)
if is_api_request(request):
# only return local book results via json so we don't cascade
@ -30,49 +36,87 @@ class Search(View):
)
return JsonResponse([r.json() for r in book_results], safe=False)
# use webfinger for mastodon style account@domain.com username
if query and re.match(regex.full_username, query):
handle_remote_webfinger(query)
if query and not search_type:
search_type = "user" if "@" in query else "book"
# do a user search
user_results = (
models.User.viewer_aware_objects(request.user)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
)
)
.filter(
similarity__gt=0.5,
)
.order_by("-similarity")[:10]
)
# any relevent lists?
list_results = (
privacy_filter(
request.user,
models.List.objects,
privacy_levels=["public", "followers"],
)
.annotate(
similarity=Greatest(
TrigramSimilarity("name", query),
TrigramSimilarity("description", query),
)
)
.filter(
similarity__gt=0.1,
)
.order_by("-similarity")[:10]
)
book_results = connector_manager.search(query, min_confidence=min_confidence)
data = {
"book_results": book_results,
"user_results": user_results,
"list_results": list_results,
"query": query or "",
endpoints = {
"book": book_search,
"user": user_search,
"list": list_search,
}
return TemplateResponse(request, "search_results.html", data)
if not search_type in endpoints:
search_type = "book"
data = {
"query": query or "",
"type": search_type,
"remote": search_remote,
}
if query:
results = endpoints[search_type](
query, request.user, min_confidence, search_remote
)
if results:
paginated = Paginator(results, PAGE_LENGTH).get_page(
request.GET.get("page")
)
data["results"] = paginated
return TemplateResponse(request, "search/{:s}.html".format(search_type), data)
def book_search(query, _, min_confidence, search_remote=False):
"""the real business is elsewhere"""
if search_remote:
return connector_manager.search(query, min_confidence=min_confidence)
results = connector_manager.local_search(query, min_confidence=min_confidence)
if not results:
return None
return [{"results": results}]
def user_search(query, viewer, *_):
"""cool kids members only user search"""
# logged out viewers can't search users
if not viewer.is_authenticated:
return models.User.objects.none()
# use webfinger for mastodon style account@domain.com username to load the user if
# they don't exist locally (handle_remote_webfinger will check the db)
if re.match(regex.full_username, query):
handle_remote_webfinger(query)
return (
models.User.viewer_aware_objects(viewer)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
)
)
.filter(
similarity__gt=0.5,
)
.order_by("-similarity")[:10]
)
def list_search(query, viewer, *_):
"""any relevent lists?"""
return (
privacy_filter(
viewer,
models.List.objects,
privacy_levels=["public", "followers"],
)
.annotate(
similarity=Greatest(
TrigramSimilarity("name", query),
TrigramSimilarity("description", query),
)
)
.filter(
similarity__gt=0.1,
)
.order_by("-similarity")[:10]
)

View File

@ -2,6 +2,7 @@
from collections import namedtuple
from django.db import IntegrityError
from django.db.models import Count, OuterRef, Subquery, F, Q
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest, HttpResponseNotFound
@ -25,10 +26,7 @@ class Shelf(View):
def get(self, request, username, shelf_identifier=None):
"""display a shelf"""
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
user = get_user_from_username(request.user, username)
shelves = privacy_filter(request.user, user.shelf_set)
@ -40,35 +38,50 @@ class Shelf(View):
return HttpResponseNotFound()
if not shelf.visible_to_user(request.user):
return HttpResponseNotFound()
books = shelf.books
# this is a constructed "all books" view, with a fake "shelf" obj
else:
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()
shelf = FakeShelf("all", _("All books"), user, books, "public")
is_self = request.user == user
if is_api_request(request):
return ActivitypubResponse(shelf.to_activity(**request.GET))
reviews = privacy_filter(
request.user,
models.Review.objects.filter(
user=user,
rating__isnull=False,
book__id=OuterRef("id"),
),
).order_by("-published_date")
books = books.annotate(rating=Subquery(reviews.values("rating")[:1]))
paginated = Paginator(
shelf.books.order_by("-updated_date"),
books.order_by("-updated_date"),
PAGE_LENGTH,
)
page = paginated.get_page(request.GET.get("page"))
data = {
"user": user,
"is_self": is_self,
"is_self": request.user == user,
"shelves": shelves.all(),
"shelf": shelf,
"books": paginated.get_page(request.GET.get("page")),
"books": page,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
}
return TemplateResponse(request, "user/shelf.html", data)
return TemplateResponse(request, "user/shelf/shelf.html", data)
@method_decorator(login_required, name="dispatch")
# pylint: disable=unused-argument

View File

@ -21,7 +21,7 @@ from .reading import edit_readthrough
class CreateStatus(View):
"""the view for *posting*"""
def get(self, request):
def get(self, request, status_type): # pylint: disable=unused-argument
"""compose view (used for delete-and-redraft"""
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
data = {"book": book}

View File

@ -10,7 +10,8 @@ def get_notification_count(request):
"""any notifications waiting?"""
return JsonResponse(
{
"count": request.user.notification_set.filter(read=False).count(),
"count": request.user.unread_notification_count,
"has_mentions": request.user.has_unread_mentions,
}
)

View File

@ -6,7 +6,6 @@ from PIL import Image
from django.contrib.auth.decorators import login_required
from django.core.files.base import ContentFile
from django.core.paginator import Paginator
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils import timezone
@ -17,7 +16,7 @@ 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
from .helpers import privacy_filter
# pylint: disable= no-self-use
@ -26,14 +25,7 @@ class User(View):
def get(self, request, username):
"""profile page for a user"""
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
# make sure we're not blocked
if is_blocked(request.user, user):
return HttpResponseNotFound()
user = get_user_from_username(request.user, username)
if is_api_request(request):
# we have a json request
@ -94,24 +86,18 @@ class Followers(View):
def get(self, request, username):
"""list of followers"""
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
# make sure we're not blocked
if is_blocked(request.user, user):
return HttpResponseNotFound()
user = get_user_from_username(request.user, username)
if is_api_request(request):
return ActivitypubResponse(user.to_followers_activity(**request.GET))
paginated = Paginator(user.followers.all(), PAGE_LENGTH)
data = {
"user": user,
"is_self": request.user.id == user.id,
"followers": user.followers.all(),
"follow_list": paginated.get_page(request.GET.get("page")),
}
return TemplateResponse(request, "user/followers.html", data)
return TemplateResponse(request, "user/relationships/followers.html", data)
class Following(View):
@ -119,24 +105,18 @@ class Following(View):
def get(self, request, username):
"""list of followers"""
try:
user = get_user_from_username(request.user, username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
# make sure we're not blocked
if is_blocked(request.user, user):
return HttpResponseNotFound()
user = get_user_from_username(request.user, username)
if is_api_request(request):
return ActivitypubResponse(user.to_following_activity(**request.GET))
paginated = Paginator(user.following.all(), PAGE_LENGTH)
data = {
"user": user,
"is_self": request.user.id == user.id,
"following": user.following.all(),
"follow_list": paginated.get_page(request.GET.get("page")),
}
return TemplateResponse(request, "user/following.html", data)
return TemplateResponse(request, "user/relationships/following.html", data)
@method_decorator(login_required, name="dispatch")