Add public following/followers pages

This commit is contained in:
Andrew Godwin 2022-12-21 20:36:10 +00:00
parent 932cfe9243
commit c9794c0fcf
8 changed files with 116 additions and 28 deletions

View file

@ -904,7 +904,11 @@ table.metadata td .emoji {
} }
.view-options { .view-options {
margin: 0 0 10px 3px; margin: 0 0 10px 0px;
}
.view-options.follows {
margin: 0 0 20px 0px;
} }
.view-options a { .view-options a {

View file

@ -161,6 +161,8 @@ urlpatterns = [
path("@<handle>/action/", identity.ActionIdentity.as_view()), path("@<handle>/action/", identity.ActionIdentity.as_view()),
path("@<handle>/rss/", identity.IdentityFeed()), path("@<handle>/rss/", identity.IdentityFeed()),
path("@<handle>/report/", report.SubmitReport.as_view()), path("@<handle>/report/", report.SubmitReport.as_view()),
path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
# Posts # Posts
path("compose/", compose.Compose.as_view(), name="compose"), path("compose/", compose.Compose.as_view(), name="compose"),
path( path(

View file

@ -0,0 +1,24 @@
{% extends "identity/view.html" %}
{% block title %}{% if self.inbound %}Followers{% else %}Following{% endif %} - {{ identity }}{% endblock %}
{% block subcontent %}
{% for identity in page_obj %}
{% include "activities/_identity.html" %}
{% empty %}
<span class="empty">
This person has no {% if self.inbound %}followers{% else %}follows{% endif %} yet.
</span>
{% endfor %}
<div class="load-more">
{% if page_obj.has_previous %}
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View file

@ -20,7 +20,7 @@
{% if request.identity %} {% if request.identity %}
{% if identity == request.identity %} {% if identity == request.identity %}
<form class="inline follow"> <form class="inline follow-profile">
<a class="button" href="{% url "settings_profile" %}">Edit Profile</a> <a class="button" href="{% url "settings_profile" %}">Edit Profile</a>
</form> </form>
{% else %} {% else %}
@ -72,12 +72,10 @@
</table> </table>
{% endif %} {% endif %}
{% if identity.config_identity.visible_follows %} {% if identity.local and identity.config_identity.visible_follows %}
<div class="stats"> <div class="view-options follows">
<ul> <a href="{{ identity.urls.following }}" {% if not inbound or not follows_page %}class="selected"{% endif %}><strong>{{ following_count }}</strong> following</a>
<li><strong>{{ following_count }}</strong> following</li> <a href="{{ identity.urls.followers }}" {% if inbound or not follows_page %}class="selected"{% endif %}><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</a>
<li><strong>{{ followers_count }}</strong> follower{{ followers_count|pluralize }}</li>
</ul>
</div> </div>
{% endif %} {% endif %}
@ -94,6 +92,8 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% block subcontent %}
{% for post in page_obj %} {% for post in page_obj %}
{% include "activities/_post.html" %} {% include "activities/_post.html" %}
{% empty %} {% empty %}
@ -114,4 +114,6 @@
{% if page_obj.has_next %} {% if page_obj.has_next %}
<div class="load-more"><a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a></div> <div class="load-more"><a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a></div>
{% endif %} {% endif %}
{% endblock %}
{% endblock %} {% endblock %}

View file

@ -21,6 +21,9 @@ def test_visible_follows_disabled(client, identity):
""" """
Tests that disabling visible follows hides it from profile Tests that disabling visible follows hides it from profile
""" """
Config.set_identity(identity, "visible_follows", True)
response = client.get(identity.urls.view)
assertContains(response, '<div class="view-options follows">', status_code=200)
Config.set_identity(identity, "visible_follows", False) Config.set_identity(identity, "visible_follows", False)
response = client.get(identity.urls.view) response = client.get(identity.urls.view)
assertNotContains(response, '<div class="stats">', status_code=200) assertNotContains(response, '<div class="view-options follows">', status_code=200)

View file

@ -219,6 +219,8 @@ class Identity(StatorModel):
class urls(urlman.Urls): class urls(urlman.Urls):
view = "/@{self.username}@{self.domain_id}/" view = "/@{self.username}@{self.domain_id}/"
action = "{view}action/" action = "{view}action/"
followers = "{view}followers/"
following = "{view}following/"
activate = "{view}activate/" activate = "{view}activate/"
admin = "/admin/identities/" admin = "/admin/identities/"
admin_edit = "{admin}{self.pk}/" admin_edit = "{admin}{self.pk}/"

View file

@ -1,5 +1,7 @@
from typing import cast from typing import cast
from django.db import models
from users.models import Follow, FollowStates, Identity from users.models import Follow, FollowStates, Identity
@ -11,6 +13,16 @@ class IdentityService:
def __init__(self, identity: Identity): def __init__(self, identity: Identity):
self.identity = identity self.identity = identity
def following(self) -> models.QuerySet[Identity]:
return Identity.objects.filter(
inbound_follows__source=self.identity
).not_deleted()
def followers(self) -> models.QuerySet[Identity]:
return Identity.objects.filter(
outbound_follows__target=self.identity
).not_deleted()
def follow_from(self, from_identity: Identity) -> Follow: def follow_from(self, from_identity: Identity) -> Follow:
""" """
Follows a user (or does nothing if already followed). Follows a user (or does nothing if already followed).

View file

@ -29,7 +29,7 @@ class ViewIdentity(ListView):
""" """
template_name = "identity/view.html" template_name = "identity/view.html"
paginate_by = 5 paginate_by = 25
def get(self, request, handle): def get(self, request, handle):
# Make sure we understand this handle # Make sure we understand this handle
@ -140,6 +140,45 @@ class IdentityFeed(Feed):
return item.published return item.published
class IdentityFollows(ListView):
"""
Shows following/followers for an identity.
"""
template_name = "identity/follows.html"
paginate_by = 25
inbound = False
def get(self, request, handle):
self.identity = by_handle_or_404(
self.request,
handle,
local=False,
)
if not Config.load_identity(self.identity).visible_follows:
raise Http404("Hidden follows")
return super().get(request, identity=self.identity)
def get_queryset(self):
if self.inbound:
return IdentityService(self.identity).followers()
else:
return IdentityService(self.identity).following()
def get_context_data(self):
context = super().get_context_data()
context["identity"] = self.identity
context["inbound"] = self.inbound
context["follows_page"] = True
context["followers_count"] = self.identity.inbound_follows.filter(
state__in=FollowStates.group_active()
).count()
context["following_count"] = self.identity.outbound_follows.filter(
state__in=FollowStates.group_active()
).count()
return context
@method_decorator(identity_required, name="dispatch") @method_decorator(identity_required, name="dispatch")
class ActionIdentity(View): class ActionIdentity(View):
def post(self, request, handle): def post(self, request, handle):