Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-04-30 14:05:20 -07:00
commit 7aff486a59
31 changed files with 247 additions and 167 deletions

View file

@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
""" What you need in the database to make it work """
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
@ -7,12 +8,14 @@ from bookwyrm.settings import DOMAIN
def init_groups():
"""permission levels"""
groups = ["admin", "moderator", "editor"]
for group in groups:
Group.objects.create(name=group)
def init_permissions():
"""permission types"""
permissions = [
{
"codename": "edit_instance_settings",
@ -69,6 +72,7 @@ def init_permissions():
def init_connectors():
"""access book data sources"""
Connector.objects.create(
identifier=DOMAIN,
name="Local",
@ -130,7 +134,11 @@ def init_federated_servers():
def init_settings():
SiteSettings.objects.create()
"""info about the instance"""
SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon",
)
class Command(BaseCommand):

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-30 17:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0072_remove_work_default_edition"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="footer_item",
field=models.TextField(blank=True, null=True),
),
]

View file

@ -19,19 +19,28 @@ class SiteSettings(models.Model):
max_length=150, default="Social Reading and Reviewing"
)
instance_description = models.TextField(default="This instance has no description.")
# about page
registration_closed_text = models.TextField(
default="Contact an administrator to get an invite"
)
code_of_conduct = models.TextField(default="Add a code of conduct here.")
privacy_policy = models.TextField(default="Add a privacy policy here.")
# registration
allow_registration = models.BooleanField(default=True)
allow_invite_requests = models.BooleanField(default=True)
# images
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
favicon = models.ImageField(upload_to="logos/", null=True, blank=True)
# footer
support_link = models.CharField(max_length=255, null=True, blank=True)
support_title = models.CharField(max_length=100, null=True, blank=True)
admin_email = models.EmailField(max_length=255, null=True, blank=True)
footer_item = models.TextField(null=True, blank=True)
@classmethod
def get(cls):

View file

@ -150,6 +150,19 @@ class User(OrderedCollectionPageMixin, AbstractUser):
"""for consistent naming"""
return not self.is_active
@property
def unread_notification_count(self):
"""count of notifications, for the templates"""
return self.notification_set.filter(read=False).count()
@property
def has_unread_mentions(self):
"""whether any of the unread notifications are conversations"""
return self.notification_set.filter(
read=False,
notification_type__in=["REPLY", "MENTION", "TAG", "REPORT"],
).exists()
activity_serializer = activitypub.Person
@classmethod

View file

@ -97,10 +97,12 @@ let BookWyrm = new class {
updateCountElement(counter, data) {
const currentCount = counter.innerText;
const count = data.count;
const hasMentions = data.has_mentions;
if (count != currentCount) {
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
counter.innerText = count;
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-danger', hasMentions);
}
}

View file

@ -1,4 +1,4 @@
{% extends 'user/user_layout.html' %}
{% extends 'user/layout.html' %}
{% load i18n %}
{% block header %}

View file

@ -135,8 +135,11 @@
<span class="is-sr-only">{% trans "Notifications" %}</span>
</span>
</span>
<span class="{% if not request.user|notification_count %}is-hidden {% endif %}tag is-danger is-medium transition-x" data-poll-wrapper>
<span data-poll="notifications">{{ request.user | notification_count }}</span>
<span
class="{% if not request.user.unread_notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x"
data-poll-wrapper
>
<span data-poll="notifications">{{ request.user.unread_notification_count }}</span>
</span>
</a>
</div>
@ -190,7 +193,7 @@
<footer class="footer">
<div class="container">
<div class="columns">
<div class="column">
<div class="column is-one-fifth">
<p>
<a href="/about">{% trans "About this server" %}</a>
</p>
@ -199,16 +202,26 @@
<a href="mailto:{{ site.admin_email }}">{% trans "Contact site admin" %}</a>
</p>
{% endif %}
<p>
<a href="https://docs.joinbookwyrm.com/">{% trans "Documentation" %}</a>
</p>
</div>
<div class="column content is-two-fifth">
{% if site.support_link %}
<div class="column">
<p>
<span class="icon icon-heart"></span>
{% blocktrans with site_name=site.name support_link=site.support_link support_title=site.support_title %}Support {{ site_name }} on <a href="{{ support_link }}" target="_blank">{{ support_title }}</a>{% endblocktrans %}
</p>
{% endif %}
<p>
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %}
</p>
</div>
{% if site.footer_item %}
<div class="column">
<p>{{ site.footer_item|safe }}</p>
</div>
{% endif %}
<div class="column">
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %}
</div>
</div>
</div>
</footer>

View file

@ -44,8 +44,15 @@
</div>
<div class="column ml-3">
<span>{% include 'snippets/book_titleby.html' %}</span>
<p>
{% include 'snippets/book_titleby.html' %}
</p>
<p>
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
</p>
<p>
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
</p>
{% include 'snippets/shelve_button/shelve_button.html' %}
</div>
</div>

View file

@ -37,16 +37,16 @@
<section class="block" id="images">
<h2 class="title is-4">{% trans "Images" %}</h2>
<div class="field is-grouped">
<div class="control">
<div class="columns">
<div class="column">
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }}
</div>
<div class="control">
<div class="column">
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }}
</div>
<div class="control">
<div class="column">
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }}
</div>
@ -69,6 +69,10 @@
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }}
</div>
<div class="control">
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
</div>
</section>
<hr aria-hidden="true">

View file

@ -1,34 +0,0 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block header %}
<h1 class="title">
{% trans "User Profile" %}
</h1>
{% endblock %}
{% block panel %}
<div class="block">
<h2 class="title">{% trans "Followers" %}</h2>
{% for follower in followers %}
<div class="block columns">
<div class="column">
<a href="{{ follower.local_path }}">
{% include 'snippets/avatar.html' with user=follower %}
{{ follower.display_name }}
</a>
({{ follower.username }})
</div>
<div class="column is-narrow">
{% include 'snippets/follow_button.html' with user=follower %}
</div>
</div>
{% endfor %}
{% if not followers.count %}
<div>{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}</div>
{% endif %}
</div>
{% include 'snippets/pagination.html' with page=followers path=request.path %}
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block header %}
<h1 class="title">
{% trans "User Profile" %}
</h1>
{% endblock %}
{% block panel %}
<div class="block">
<h2 class="title">{% trans "Following" %}</h2>
{% for follower in user.following.all %}
<div class="block columns">
<div class="column">
<a href="{{ follower.local_path }}">
{% include 'snippets/avatar.html' with user=follower %}
{{ follower.display_name }}
</a>
({{ follower.username }})
</div>
<div class="column">
{% include 'snippets/follow_button.html' with user=follower %}
</div>
</div>
{% endfor %}
{% if not following.count %}
<div>{% blocktrans with username=user|username %}{{ username }} isn't following any users{% endblocktrans %}</div>
{% endif %}
</div>
{% include 'snippets/pagination.html' with page=following path=request.path %}
{% endblock %}

View file

@ -7,7 +7,11 @@
{% block content %}
<header class="block">
{% block header %}{% endblock %}
{% block header %}
<h1 class="title">
{% trans "User Profile" %}
</h1>
{% endblock %}
</header>
{# user bio #}
@ -41,8 +45,9 @@
</div>
{% endif %}
</div>
{% block tabs %}
{% with user|username as username %}
{% if 'user/'|add:username|add:'/books' not in request.path and 'user/'|add:username|add:'/shelf' not in request.path %}
<nav class="tabs">
<ul>
{% url 'user-feed' user|username as url %}
@ -70,8 +75,8 @@
{% endif %}
</ul>
</nav>
{% endif %}
{% endwith %}
{% endblock %}
{% block panel %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'user/user_layout.html' %}
{% extends 'user/layout.html' %}
{% load i18n %}
{% block header %}
@ -23,7 +23,7 @@
{% block panel %}
<section class="block content">
<section class="block">
<form name="create-list" method="post" action="{% url 'lists' %}" class="box is-hidden" id="create-list">
<header class="columns">
<h3 class="title column">{% trans "Create list" %}</h3>

View file

@ -0,0 +1,14 @@
{% extends 'user/relationships/layout.html' %}
{% load i18n %}
{% block header %}
<h1 class="title">
{% trans "Followers" %}
</h1>
{% endblock %}
{% block nullstate %}
<div>
{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}
</div>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'user/relationships/layout.html' %}
{% load i18n %}
{% block header %}
<h1 class="title">
{% trans "Following" %}
</h1>
{% endblock %}
{% block nullstate %}
<div>
{% blocktrans with username=user.display_name %}{{ username }} isn't following any users{% endblocktrans %}
</div>
{% endblock %}

View file

@ -0,0 +1,46 @@
{% extends 'user/layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block tabs %}
{% with user|username as username %}
<nav class="tabs">
<ul>
{% url 'user-followers' user|username as url %}
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Followers" %}</a>
</li>
{% url 'user-following' user|username as url %}
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Following" %}</a>
</li>
</ul>
</nav>
{% endwith %}
{% endblock %}
{% block panel %}
<div class="block">
{% for follow in follow_list %}
<div class="block columns">
<div class="column">
<a href="{{ follower.local_path }}">
{% include 'snippets/avatar.html' with user=follow %}
{{ follow.display_name }}
</a>
({{ follow.username }})
</div>
<div class="column is-narrow">
{% include 'snippets/follow_button.html' with user=follow %}
</div>
</div>
{% endfor %}
{% if not follow_list %}
{% block nullstate %}
{% endblock %}
{% endif %}
</div>
{% include 'snippets/pagination.html' with page=follow_list path=request.path %}
{% endblock %}

View file

@ -1,21 +1,21 @@
{% extends 'user/user_layout.html' %}
{% extends 'user/layout.html' %}
{% load bookwyrm_tags %}
{% load humanize %}
{% load i18n %}
{% block title %}
{% include 'user/books_header.html' %}
{% include 'user/shelf/books_header.html' %}
{% endblock %}
{% block header %}
<header class="columns">
<h1 class="title">
{% include 'user/books_header.html' %}
{% include 'user/shelf/books_header.html' %}
</h1>
</header>
{% endblock %}
{% block panel %}
{% block tabs %}
<div class="block columns">
<div class="column">
<div class="tabs">
@ -39,9 +39,11 @@
</div>
{% endif %}
</div>
{% endblock %}
{% block panel %}
<div class="block">
{% include 'user/create_shelf_form.html' with controls_text='create-shelf-form' %}
{% include 'user/shelf/create_shelf_form.html' with controls_text='create-shelf-form' %}
</div>
<div class="block columns is-mobile">
@ -62,7 +64,7 @@
</div>
<div class="block">
{% include 'user/edit_shelf_form.html' with controls_text="edit-shelf-form" %}
{% include 'user/shelf/edit_shelf_form.html' with controls_text="edit-shelf-form" %}
</div>
<div class="block">

View file

@ -1,4 +1,4 @@
{% extends 'user/user_layout.html' %}
{% extends 'user/layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
@ -11,7 +11,7 @@
</div>
{% if is_self %}
<div class="column is-narrow">
<a href="/preferences/profile">
<a href="{% url 'prefs-profile' %}">
<span class="icon icon-pencil" title="Edit profile">
<span class="is-sr-only">{% trans "Edit profile" %}</span>
</span>
@ -25,7 +25,7 @@
{% if user.bookwyrm_user %}
<div class="block">
<h2 class="title">
{% include 'user/books_header.html' %}
{% include 'user/shelf/books_header.html' %}
</h2>
<div class="columns">
{% for shelf in shelves %}

View file

@ -1,5 +1,6 @@
{% load i18n %}
{% load humanize %}
{% load bookwyrm_tags %}
<div class="media block">
<div class="media-left">
@ -12,8 +13,19 @@
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
<p>
<a href="{{ user.local_path }}/followers">{% blocktrans count counter=user.followers.count %}{{ counter }} follower{% plural %}{{ counter }} followers{% endblocktrans %}</a>,
<a href="{{ user.local_path }}/following">{% blocktrans with counter=user.following.count %}{{ counter }} following{% endblocktrans %}</a>
{% if is_self %}
<a href="{% url 'user-followers' user|username %}">{% blocktrans count counter=user.followers.count %}{{ counter }} follower{% plural %}{{ counter }} followers{% endblocktrans %}</a>,
<a href="{% url 'user-following' user|username %}">{% blocktrans with counter=user.following.count %}{{ counter }} following{% endblocktrans %}</a>
{% elif request.user.is_authenticated %}
{% mutuals_count user as mutuals %}
<a href="{% url 'user-followers' user|username %}">
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
</a>
{% endif %}
</p>
</div>
</div>

View file

@ -235,3 +235,12 @@ def get_lang():
"""get current language, strip to the first two letters"""
language = utils.translation.get_language()
return language[0 : language.find("-")]
@register.simple_tag(takes_context=True)
def mutuals_count(context, user):
"""how many users that you follow, follow them"""
viewer = context["request"].user
if not viewer.is_authenticated:
return None
return user.followers.filter(id__in=viewer.following.all()).count()

View file

@ -3,6 +3,7 @@ import json
from unittest.mock import patch
import pathlib
from django.db.models import Q
from django.http import Http404
from django.test import TestCase
from django.test.client import RequestFactory
import responses
@ -67,7 +68,7 @@ class ViewsHelpers(TestCase):
views.helpers.get_user_from_username(self.local_user, "mouse@local.com"),
self.local_user,
)
with self.assertRaises(models.User.DoesNotExist):
with self.assertRaises(Http404):
views.helpers.get_user_from_username(self.local_user, "mojfse@example.com")
def test_is_api_request(self, _):

View file

@ -6,6 +6,7 @@ from PIL import Image
from django.contrib.auth.models import AnonymousUser
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http.response import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -76,8 +77,8 @@ class UserViews(TestCase):
self.rat.blocks.add(self.local_user)
with patch("bookwyrm.views.user.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "rat")
self.assertEqual(result.status_code, 404)
with self.assertRaises(Http404):
view(request, "rat")
def test_followers_page(self):
"""there are so many views, this just makes sure it LOADS"""
@ -105,8 +106,8 @@ class UserViews(TestCase):
self.rat.blocks.add(self.local_user)
with patch("bookwyrm.views.user.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "rat")
self.assertEqual(result.status_code, 404)
with self.assertRaises(Http404):
view(request, "rat")
def test_following_page(self):
"""there are so many views, this just makes sure it LOADS"""
@ -134,8 +135,8 @@ class UserViews(TestCase):
self.rat.blocks.add(self.local_user)
with patch("bookwyrm.views.user.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "rat")
self.assertEqual(result.status_code, 404)
with self.assertRaises(Http404):
view(request, "rat")
def test_edit_user_page(self):
"""there are so many views, this just makes sure it LOADS"""
@ -166,7 +167,6 @@ class UserViews(TestCase):
self.assertEqual(self.local_user.name, "New Name")
self.assertEqual(self.local_user.email, "wow@email.com")
# idk how to mock the upload form, got tired of triyng to make it work
def test_edit_user_avatar(self):
"""use a form to update a user"""
view = views.EditUser.as_view()

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
@ -62,7 +62,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))
@ -94,7 +94,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()
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()
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()
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()
try:
follow_request = models.UserFollowRequest.objects.get(

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

View file

@ -25,10 +25,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()
shelves = privacy_filter(request.user, user.shelf_set)
@ -68,7 +65,7 @@ class Shelf(View):
"books": paginated.get_page(request.GET.get("page")),
}
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

@ -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()
if is_api_request(request):
# we have a json request
@ -94,14 +86,7 @@ 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()
if is_api_request(request):
return ActivitypubResponse(user.to_followers_activity(**request.GET))
@ -110,9 +95,9 @@ class Followers(View):
data = {
"user": user,
"is_self": request.user.id == user.id,
"followers": paginated.page(request.GET.get("page", 1)),
"follow_list": paginated.page(request.GET.get("page", 1)),
}
return TemplateResponse(request, "user/followers.html", data)
return TemplateResponse(request, "user/relationships/followers.html", data)
class Following(View):
@ -120,25 +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()
if is_api_request(request):
return ActivitypubResponse(user.to_following_activity(**request.GET))
paginated = Paginator(user.followers.all(), PAGE_LENGTH)
paginated = Paginator(user.following.all(), PAGE_LENGTH)
data = {
"user": user,
"is_self": request.user.id == user.id,
"following": paginated.page(request.GET.get("page", 1)),
"follow_list": paginated.page(request.GET.get("page", 1)),
}
return TemplateResponse(request, "user/following.html", data)
return TemplateResponse(request, "user/relationships/following.html", data)
@method_decorator(login_required, name="dispatch")