Continue to refactor

This commit is contained in:
Andrew Godwin 2023-04-27 19:09:16 -06:00
parent 5d68e3aaac
commit 0225c6e8ba
39 changed files with 364 additions and 603 deletions

View file

@ -72,7 +72,12 @@ class PostService:
def unboost_as(self, identity: Identity): def unboost_as(self, identity: Identity):
self.uninteract_as(identity, PostInteraction.Types.boost) self.uninteract_as(identity, PostInteraction.Types.boost)
def context(self, identity: Identity | None) -> tuple[list[Post], list[Post]]: def context(
self,
identity: Identity | None,
num_ancestors: int = 10,
num_descendants: int = 50,
) -> tuple[list[Post], list[Post]]:
""" """
Returns ancestor/descendant information. Returns ancestor/descendant information.
@ -82,7 +87,6 @@ class PostService:
If identity is provided, includes mentions/followers-only posts they If identity is provided, includes mentions/followers-only posts they
can see. Otherwise, shows unlisted and above only. can see. Otherwise, shows unlisted and above only.
""" """
num_ancestors = 10
num_descendants = 50 num_descendants = 50
# Retrieve ancestors via parent walk # Retrieve ancestors via parent walk
ancestors: list[Post] = [] ancestors: list[Post] = []

View file

@ -16,7 +16,7 @@ from activities.models import (
from core.files import blurhash_image, resize_image from core.files import blurhash_image, resize_image
from core.html import FediverseHtmlParser from core.html import FediverseHtmlParser
from core.models import Config from core.models import Config
from users.shortcuts import by_handle_or_404 from users.shortcuts import by_handle_for_user_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -167,10 +167,10 @@ class Compose(FormView):
) )
# Add their own timeline event for immediate visibility # Add their own timeline event for immediate visibility
TimelineEvent.add_post(self.identity, post) TimelineEvent.add_post(self.identity, post)
return redirect("/") return redirect(self.identity.urls.view)
def dispatch(self, request, handle=None, post_id=None, *args, **kwargs): def dispatch(self, request, handle=None, post_id=None, *args, **kwargs):
self.identity = by_handle_or_404(self.request, handle, local=True, fetch=False) self.identity = by_handle_for_user_or_404(self.request, handle)
self.post_obj = None self.post_obj = None
if handle and post_id: if handle and post_id:
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id) self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
@ -190,94 +190,7 @@ class Compose(FormView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["reply_to"] = self.reply_to context["reply_to"] = self.reply_to
context["identity"] = self.identity context["identity"] = self.identity
context["section"] = "compose"
if self.post_obj: if self.post_obj:
context["post"] = self.post_obj context["post"] = self.post_obj
return context return context
@method_decorator(login_required, name="dispatch")
class ImageUpload(FormView):
"""
Handles image upload - returns a new input type hidden to embed in
the main form that references an orphaned PostAttachment
"""
template_name = "activities/_image_upload.html"
class form_class(forms.Form):
image = forms.ImageField(
widget=forms.FileInput(
attrs={
"_": f"""
on change
if me.files[0].size > {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB * 1024 ** 2}
add [@disabled=] to #upload
remove <ul.errorlist/>
make <ul.errorlist/> called errorlist
make <li/> called error
set size_in_mb to (me.files[0].size / 1024 / 1024).toFixed(2)
put 'File must be {settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB}MB or less (actual: ' + size_in_mb + 'MB)' into error
put error into errorlist
put errorlist before me
else
remove @disabled from #upload
remove <ul.errorlist/>
end
end
"""
}
)
)
description = forms.CharField(required=False)
def clean_image(self):
value = self.cleaned_data["image"]
max_mb = settings.SETUP.MEDIA_MAX_IMAGE_FILESIZE_MB
max_bytes = max_mb * 1024 * 1024
if value.size > max_bytes:
# Erase the file from our data to stop trying to show it again
self.files = {}
raise forms.ValidationError(
f"File must be {max_mb}MB or less (actual: {value.size / 1024 ** 2:.2f})"
)
return value
def form_invalid(self, form):
return super().form_invalid(form)
def form_valid(self, form):
# Make a PostAttachment
main_file = resize_image(
form.cleaned_data["image"],
size=(2000, 2000),
cover=False,
)
thumbnail_file = resize_image(
form.cleaned_data["image"],
size=(400, 225),
cover=True,
)
attachment = PostAttachment.objects.create(
blurhash=blurhash_image(thumbnail_file),
mimetype="image/webp",
width=main_file.image.width,
height=main_file.image.height,
name=form.cleaned_data.get("description"),
state=PostAttachmentStates.fetched,
author=self.identity,
)
attachment.file.save(
main_file.name,
main_file,
)
attachment.thumbnail.save(
thumbnail_file.name,
thumbnail_file,
)
attachment.save()
# Return the response, with a hidden input plus a note
return render(
self.request, "activities/_image_uploaded.html", {"attachment": attachment}
)

View file

@ -1,26 +0,0 @@
from django.views.generic import ListView
from activities.models import Hashtag
class ExploreTag(ListView):
template_name = "activities/explore_tag.html"
extra_context = {
"current_page": "explore",
"allows_refresh": True,
}
paginate_by = 20
def get_queryset(self):
return (
Hashtag.objects.public()
.filter(
stats__total__gt=0,
)
.order_by("-stats__total")
)[:20]
class Explore(ExploreTag):
pass

View file

@ -1,38 +0,0 @@
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.views.generic import View
from activities.models.hashtag import Hashtag
from django.contrib.auth.decorators import login_required
@method_decorator(login_required, name="dispatch")
class HashtagFollow(View):
"""
Follows/unfollows a hashtag with the current identity
"""
undo = False
def post(self, request: HttpRequest, hashtag):
hashtag = get_object_or_404(
Hashtag,
pk=hashtag,
)
follow = None
if self.undo:
request.identity.hashtag_follows.filter(hashtag=hashtag).delete()
else:
follow = request.identity.hashtag_follows.get_or_create(hashtag=hashtag)
# Return either a redirect or a HTMX snippet
if request.htmx:
return render(
request,
"activities/_hashtag_follow.html",
{
"hashtag": hashtag,
"follow": follow,
},
)
return redirect(hashtag.urls.view)

View file

@ -47,7 +47,9 @@ class Individual(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
ancestors, descendants = PostService(self.post_obj).context(None) ancestors, descendants = PostService(self.post_obj).context(
identity=None, num_ancestors=2
)
context.update( context.update(
{ {
@ -69,27 +71,3 @@ class Individual(TemplateView):
canonicalise(self.post_obj.to_ap(), include_security=True), canonicalise(self.post_obj.to_ap(), include_security=True),
content_type="application/activity+json", content_type="application/activity+json",
) )
@method_decorator(login_required, name="dispatch")
class Delete(TemplateView):
"""
Deletes a post
"""
template_name = "activities/post_delete.html"
def dispatch(self, request, handle, post_id):
# Make sure the request identity owns the post!
if handle != request.identity.handle:
raise PermissionDenied("Post author is not requestor")
self.identity = by_handle_or_404(self.request, handle, local=False)
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
return super().dispatch(request)
def get_context_data(self):
return {"post": self.post_obj}
def post(self, request):
PostService(self.post_obj).delete()
return redirect("/")

View file

@ -15,7 +15,7 @@ class Search(FormView):
) )
def form_valid(self, form): def form_valid(self, form):
searcher = SearchService(form.cleaned_data["query"], self.request.identity) searcher = SearchService(form.cleaned_data["query"], identity=None)
# Render results # Render results
context = self.get_context_data(form=form) context = self.get_context_data(form=form)
context["results"] = searcher.search_all() context["results"] = searcher.search_all()

View file

@ -1,3 +1,5 @@
from typing import Any
from django.http import Http404
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -8,6 +10,7 @@ from activities.services import TimelineService
from core.decorators import cache_page from core.decorators import cache_page
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from users.models import Bookmark, HashtagFollow, Identity from users.models import Bookmark, HashtagFollow, Identity
from users.views.base import IdentityViewMixin
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
@ -46,74 +49,15 @@ class Tag(ListView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
return TimelineService(self.request.identity).hashtag(self.hashtag) return TimelineService(None).hashtag(self.hashtag)
def get_context_data(self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
context["hashtag"] = self.hashtag context["hashtag"] = self.hashtag
context["interactions"] = PostInteraction.get_post_interactions(
context["page_obj"], self.request.identity
)
context["bookmarks"] = Bookmark.for_identity(
self.request.identity, context["page_obj"]
)
context["follow"] = HashtagFollow.maybe_get(
self.request.identity,
self.hashtag,
)
return context return context
@method_decorator( class Notifications(IdentityViewMixin, ListView):
cache_page("cache_timeout_page_timeline", public_only=True), name="dispatch"
)
class Local(ListView):
template_name = "activities/local.html"
extra_context = {
"current_page": "local",
"allows_refresh": True,
}
paginate_by = 25
def get_queryset(self):
return TimelineService(self.request.identity).local()
def get_context_data(self):
context = super().get_context_data()
context["interactions"] = PostInteraction.get_post_interactions(
context["page_obj"], self.request.identity
)
context["bookmarks"] = Bookmark.for_identity(
self.request.identity, context["page_obj"]
)
return context
@method_decorator(login_required, name="dispatch")
class Federated(ListView):
template_name = "activities/federated.html"
extra_context = {
"current_page": "federated",
"allows_refresh": True,
}
paginate_by = 25
def get_queryset(self):
return TimelineService(self.request.identity).federated()
def get_context_data(self):
context = super().get_context_data()
context["interactions"] = PostInteraction.get_post_interactions(
context["page_obj"], self.request.identity
)
context["bookmarks"] = Bookmark.for_identity(
self.request.identity, context["page_obj"]
)
return context
@method_decorator(login_required, name="dispatch")
class Notifications(ListView):
template_name = "activities/notifications.html" template_name = "activities/notifications.html"
extra_context = { extra_context = {
"current_page": "notifications", "current_page": "notifications",
@ -125,7 +69,6 @@ class Notifications(ListView):
"boosted": TimelineEvent.Types.boosted, "boosted": TimelineEvent.Types.boosted,
"mentioned": TimelineEvent.Types.mentioned, "mentioned": TimelineEvent.Types.mentioned,
"liked": TimelineEvent.Types.liked, "liked": TimelineEvent.Types.liked,
"identity_created": TimelineEvent.Types.identity_created,
} }
def get_queryset(self): def get_queryset(self):
@ -143,7 +86,7 @@ class Notifications(ListView):
for type_name, type in self.notification_types.items(): for type_name, type in self.notification_types.items():
if notification_options.get(type_name, True): if notification_options.get(type_name, True):
types.append(type) types.append(type)
return TimelineService(self.request.identity).notifications(types) return TimelineService(self.identity).notifications(types)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -164,12 +107,6 @@ class Notifications(ListView):
events.append(event) events.append(event)
# Retrieve what kinds of things to show # Retrieve what kinds of things to show
context["events"] = events context["events"] = events
context["identity"] = self.identity
context["notification_options"] = self.request.session["notification_options"] context["notification_options"] = self.request.session["notification_options"]
context["interactions"] = PostInteraction.get_event_interactions(
context["page_obj"],
self.request.identity,
)
context["bookmarks"] = Bookmark.for_identity(
self.request.identity, context["page_obj"], "subject_post_id"
)
return context return context

View file

@ -20,16 +20,6 @@ def vary_by_ap_json(request, *args, **kwargs) -> str:
return "not_ap" return "not_ap"
def vary_by_identity(request, *args, **kwargs) -> str:
"""
Return a cache usable string token that is different based upon the
request.identity
"""
if request.identity:
return f"ident{request.identity.pk}"
return "identNone"
def cache_page( def cache_page(
timeout: int | str = "cache_timeout_page_default", timeout: int | str = "cache_timeout_page_default",
*, *,

View file

@ -30,7 +30,7 @@ class About(TemplateView):
template_name = "about.html" template_name = "about.html"
def get_context_data(self): def get_context_data(self):
service = TimelineService(self.request.identity) service = TimelineService(None)
return { return {
"current_page": "about", "current_page": "about",
"content": mark_safe( "content": mark_safe(

42
static/img/apps/elk.svg Executable file
View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="250"
height="250"
fill="none"
version="1.1"
id="svg30"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs34" />
<mask
id="a"
width="240"
height="234"
x="4"
y="1"
maskUnits="userSpaceOnUse"
style="mask-type:alpha">
<path
fill="#D9D9D9"
d="M244 123c0 64.617-38.383 112-103 112-64.617 0-103-30.883-103-95.5C38 111.194-8.729 36.236 8 16 29.46-9.959 88.689 6 125 6c64.617 0 119 52.383 119 117Z"
id="path19" />
</mask>
<g
mask="url(#a)"
id="g28"
transform="matrix(0.90923731,0,0,1.0049564,13.520015,-3.1040835)">
<path
fill="#ea9e44"
d="m 116.94,88.1 c -13.344,1.552 -20.436,-2.019 -24.706,10.71 0,0 14.336,21.655 52.54,21.112 -2.135,8.848 -1.144,15.368 -1.144,23.207 0,26.079 -20.589,48.821 -65.961,48.821 -23.03,0 -51.015,4.191 -72.367,15.911 -15.175,8.305 -27.048,20.336 -32.302,37.023 l 5.956,8.461 11.4,0.155 v 47.889 l -13.91,21.966 3.998,63.645 H -6.364 L -5.22,335.773 C 1.338,331.892 16.36,321.802 29.171,306.279 46.557,285.4 59.902,255.052 44.193,217.486 l 11.744,-5.045 c 12.887,30.814 8.388,57.514 -2.898,79.013 21.58,-0.698 40.11,-2.095 55.819,-4.734 l -3.584,-43.698 12.659,-1.087 L 129.98,387 h 13.116 l 2.212,-94.459 c 10.447,-4.502 34.239,-21.034 45.372,-78.47 1.372,-6.986 2.135,-12.885 2.516,-17.93 1.754,-12.806 2.745,-27.243 3.051,-43.698 l -18.683,-5.976 h 57.42 l 5.567,-12.807 c -5.414,0.233 -11.896,-2.639 -11.896,-2.639 l 1.297,-6.209 H 242 L 176.801,90.428 c -7.244,2.794 -14.87,6.442 -20.208,10.866 -4.27,-3.105 -19.063,-12.807 -39.653,-13.195 z"
id="path22" />
<path
fill="#c16929"
d="M 6.217,24.493 18.494,21 c 5.948,21.577 13.345,33.375 22.648,39.352 8.388,5.099 19.75,5.239 31.799,4.579 C 69.433,63.767 66.154,62.137 63.104,59.886 56.317,54.841 50.522,46.458 46.175,31.246 l 12.201,-3.649 c 3.279,11.488 7.092,18.085 12.201,21.888 5.11,3.726 11.286,4.657 18.606,5.433 13.726,1.553 30.884,2.174 52.312,12.264 2.898,1.086 5.872,2.483 8.769,4.036 -0.381,-0.776 -0.762,-1.553 -1.296,-2.406 -3.66,-5.822 -10.828,-11.953 -24.097,-16.92 l 4.27,-12.109 c 21.581,7.917 30.121,19.171 33.553,28.097 3.965,10.168 1.525,18.124 1.525,18.124 -3.05,1.009 -6.1,2.406 -9.608,3.492 -6.634,-4.579 -12.887,-8.033 -18.835,-10.75 C 113.814,70.442 92.31,76.108 73.246,77.893 58.91,79.213 45.794,78.591 34.432,71.295 23.222,64.155 13.385,50.495 6.217,24.493 Z"
id="path24" />
<path
fill="#c16929"
d="M 90.098,45.294 C 87.582,39.55 86.057,32.487 86.743,23.794 l 12.659,0.932 c -0.763,10.555 2.897,17.696 7.015,22.353 -5.338,-0.931 -10.447,-1.04 -16.319,-1.785 z m 80.069,-1.32 8.312,-9.702 c 21.58,19.094 8.159,46.415 8.159,46.415 l -11.819,-1.32 c -0.382,-6.24 -1.144,-17.836 -6.635,-24.371 3.584,1.84 6.635,3.865 9.99,6.908 0,-5.666 -1.754,-12.341 -8.007,-17.93 z"
id="path26" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
static/img/apps/ivory.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/img/apps/tusky.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -5,9 +5,6 @@ from django.urls import include, path, re_path
from activities.views import ( from activities.views import (
compose, compose,
debug, debug,
explore,
follows,
hashtags,
posts, posts,
search, search,
timelines, timelines,
@ -22,28 +19,21 @@ from users.views import (
announcements, announcements,
auth, auth,
identity, identity,
report,
settings, settings,
) )
from users.views.settings import follows
urlpatterns = [ urlpatterns = [
path("", core.homepage), path("", core.homepage),
path("robots.txt", core.RobotsTxt.as_view()), path("robots.txt", core.RobotsTxt.as_view()),
# Activity views # Activity views
path("notifications/", timelines.Notifications.as_view(), name="notifications"), path(
path("local/", timelines.Local.as_view(), name="local"), "@<handle>/notifications/",
path("federated/", timelines.Federated.as_view(), name="federated"), timelines.Notifications.as_view(),
name="notifications",
),
path("search/", search.Search.as_view(), name="search"), path("search/", search.Search.as_view(), name="search"),
path("tags/<hashtag>/", timelines.Tag.as_view(), name="tag"), path("tags/<hashtag>/", timelines.Tag.as_view(), name="tag"),
path("tags/<hashtag>/follow/", hashtags.HashtagFollow.as_view()),
path("tags/<hashtag>/unfollow/", hashtags.HashtagFollow.as_view(undo=True)),
path("explore/", explore.Explore.as_view(), name="explore"),
path("explore/tags/", explore.ExploreTag.as_view(), name="explore-tag"),
path(
"follows/",
follows.Follows.as_view(),
name="follows",
),
# Settings views # Settings views
path( path(
"settings/", "settings/",
@ -70,6 +60,11 @@ urlpatterns = [
settings.InterfacePage.as_view(), settings.InterfacePage.as_view(),
name="settings_interface", name="settings_interface",
), ),
path(
"@<handle>/settings/follows/",
settings.FollowsPage.as_view(),
name="settings_follows",
),
path( path(
"@<handle>/settings/import_export/", "@<handle>/settings/import_export/",
settings.ImportExportPage.as_view(), settings.ImportExportPage.as_view(),
@ -240,22 +235,12 @@ urlpatterns = [
path("@<handle>/", identity.ViewIdentity.as_view()), path("@<handle>/", identity.ViewIdentity.as_view()),
path("@<handle>/inbox/", activitypub.Inbox.as_view()), path("@<handle>/inbox/", activitypub.Inbox.as_view()),
path("@<handle>/outbox/", activitypub.Outbox.as_view()), path("@<handle>/outbox/", activitypub.Outbox.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>/following/", identity.IdentityFollows.as_view(inbound=False)), path("@<handle>/following/", identity.IdentityFollows.as_view(inbound=False)),
path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)), path("@<handle>/followers/", identity.IdentityFollows.as_view(inbound=True)),
# Posts # Posts
path("@<handle>/compose/", compose.Compose.as_view(), name="compose"), path("@<handle>/compose/", compose.Compose.as_view(), name="compose"),
path(
"@<handle>/compose/image_upload/",
compose.ImageUpload.as_view(),
name="compose_image_upload",
),
path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()), path("@<handle>/posts/<int:post_id>/", posts.Individual.as_view()),
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
# Authentication # Authentication
path("auth/login/", auth.Login.as_view(), name="login"), path("auth/login/", auth.Login.as_view(), name="login"),
path("auth/logout/", auth.Logout.as_view(), name="logout"), path("auth/logout/", auth.Logout.as_view(), name="logout"),

View file

@ -1,9 +0,0 @@
{% if post.pk in bookmarks %}
<a title="Unbookmark" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_unbookmark }}" hx-swap="outerHTML" tabindex="0">
<i class="fa-solid fa-bookmark"></i>
</a>
{% else %}
<a title="Bookmark" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ post.urls.action_bookmark }}" hx-swap="outerHTML" tabindex="0">
<i class="fa-regular fa-bookmark"></i>
</a>
{% endif %}

View file

@ -1,9 +0,0 @@
{% if follow %}
<button title="Unfollow" class="active" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ hashtag.urls.unfollow }}" hx-swap="outerHTML" tabindex="0">
Unfollow
</button>
{% else %}
<button title="Follow" hx-trigger="click, keyup[key=='Enter']" hx-post="{{ hashtag.urls.follow }}" hx-swap="outerHTML" tabindex="0">
Follow
</button>
{% endif %}

View file

@ -1,15 +0,0 @@
<form
hx-encoding='multipart/form-data'
hx-post='{% url "compose_image_upload" %}'
hx-target="this"
hx-swap="outerHTML"
_="on htmx:xhr:progress(loaded, total)
set #attachmentProgress.value to (loaded/total)*100">
{% csrf_token %}
{% include "forms/_field.html" with field=form.image %}
{% include "forms/_field.html" with field=form.description %}
<div class="buttons">
<button id="upload" _="on click show #attachmentProgress with display:block then hide me">Upload</button>
<progress id="attachmentProgress" value="0" max="100"></progress>
</div>
</form>

View file

@ -1,19 +0,0 @@
<div class="uploaded-image">
<input type="hidden" name="attachment" value="{{ attachment.pk }}">
<img src="{{ attachment.thumbnail_url.relative }}">
<p>
{{ attachment.name|default:"(no description)" }}
</p>
<div class="buttons">
<button class="button delete left" _="on click remove closest .uploaded-image">Remove</button>
</div>
</div>
{% if request.htmx %}
<button class="add-image"
hx-get='{% url "compose_image_upload" %}'
hx-target="this"
hx-swap="outerHTML"
_="on load if length of <.uploaded-image/> > 3 then hide me">
Add Image
</button>
{% endif %}

View file

@ -76,15 +76,15 @@
<div class="actions"> <div class="actions">
<a title="Replies"> <a title="Replies">
<i class="fa-solid fa-reply"></i> <i class="fa-solid fa-reply"></i>
<span class="like-count">{{ post.stats_with_defaults.replies }}</span> <span class="like-count">{{ post.stats_with_defaults.replies|default:"0" }}</span>
</a> </a>
<a title="Likes"> <a title="Likes">
<i class="fa-solid fa-star"></i> <i class="fa-solid fa-star"></i>
<span class="like-count">{{ post.stats_with_defaults.likes }}</span> <span class="like-count">{{ post.stats_with_defaults.likes|default:"0" }}</span>
</a> </a>
<a title="Boosts"> <a title="Boosts">
<i class="fa-solid fa-retweet"></i> <i class="fa-solid fa-retweet"></i>
<span class="like-count">{{ post.stats_with_defaults.boosts }}</span> <span class="like-count">{{ post.stats_with_defaults.boosts|default:"0" }}</span>
</a> </a>
<a title="Menu" class="menu" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" role="menuitem" aria-haspopup="menu" tabindex="0"> <a title="Menu" class="menu" _="on click or keyup[key is 'Enter'] toggle .enabled on the next <menu/> then halt" role="menuitem" aria-haspopup="menu" tabindex="0">
<i class="fa-solid fa-bars"></i> <i class="fa-solid fa-bars"></i>
@ -96,14 +96,7 @@
<a href="{{ post.urls.action_report }}" role="menuitem"> <a href="{{ post.urls.action_report }}" role="menuitem">
<i class="fa-solid fa-flag"></i> Report <i class="fa-solid fa-flag"></i> Report
</a> </a>
{% if post.author == request.identity %} {% if not post.local and post.url %}
<a href="{{ post.urls.action_edit }}" role="menuitem">
<i class="fa-solid fa-pen-to-square"></i> Edit
</a>
<a href="{{ post.urls.action_delete }}" role="menuitem">
<i class="fa-solid fa-trash"></i> Delete
</a>
{% elif not post.local and post.url %}
<a href="{{ post.url }}" role="menuitem"> <a href="{{ post.url }}" role="menuitem">
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original <i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
</a> </a>

View file

@ -1,41 +1,26 @@
{% extends "base.html" %} {% extends "settings/base.html" %}
{% block title %}Compose{% endblock %} {% block title %}Compose{% endblock %}
{% block content %} {% block settings_content %}
<form action="." method="POST"> <form action="." method="POST">
{% csrf_token %} {% csrf_token %}
<fieldset> <fieldset>
<legend>Content</legend> <legend>Compose</legend>
{% if reply_to %} {% if reply_to %}
<label>Replying to</label> <label>Replying to</label>
{% include "activities/_mini_post.html" with post=reply_to %} {% include "activities/_mini_post.html" with post=reply_to %}
{% endif %} {% endif %}
<p><i>For more advanced posting options, like editing and image uploads, please use an app.</i></p>
{{ form.reply_to }} {{ form.reply_to }}
{{ form.id }} {{ form.id }}
{% include "forms/_field.html" with field=form.text %} {% include "forms/_field.html" with field=form.text %}
{% include "forms/_field.html" with field=form.content_warning %} {% include "forms/_field.html" with field=form.content_warning %}
{% include "forms/_field.html" with field=form.visibility %} {% include "forms/_field.html" with field=form.visibility %}
</fieldset> </fieldset>
<fieldset>
<legend>Images</legend>
{% if post %}
{% for attachment in post.attachments.all %}
{% include "activities/_image_uploaded.html" %}
{% endfor %}
{% endif %}
{% if not post or post.attachments.count < 4 %}
<button class="add-image"
hx-get='{% url "compose_image_upload" handle=identity.handle %}'
hx-target="this"
hx-swap="outerHTML">
Add Image
</button>
{% endif %}
</fieldset>
<div class="buttons"> <div class="buttons">
<span id="character-counter">{{ config.post_length }}</span> <span id="character-counter">{{ config.post_length }}</span>
<button id="post-button">{% if post %}Save Edits{% elif config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button> <button id="post-button">{% if post %}Save Edits{% else %}Post{% endif %}</button>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,3 +0,0 @@
{% extends "activities/local.html" %}
{% block title %}Federated Timeline{% endblock %}

View file

@ -1,25 +0,0 @@
{% extends "base.html" %}
{% block title %}Local Timeline{% endblock %}
{% block content %}
{% if page_obj.number == 1 %}
{% include "_announcements.html" %}
{% endif %}
{% for post in page_obj %}
{% include "activities/_post.html" with feedindex=forloop.counter %}
{% empty %}
No posts yet.
{% endfor %}
<div class="pagination">
{% if page_obj.has_previous and not request.htmx %}
<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 }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
{% endif %}
</div>
{% endblock %}

View file

@ -1,12 +1,13 @@
{% extends "base.html" %} {% extends "settings/base.html" %}
{% block title %}Notifications{% endblock %} {% block title %}Notifications{% endblock %}
{% block content %} {% block settings_content %}
{% if page_obj.number == 1 %} {% if page_obj.number == 1 %}
{% include "_announcements.html" %} {% include "_announcements.html" %}
{% endif %} {% endif %}
<section class="invisible">
<div class="view-options"> <div class="view-options">
{% if notification_options.followed %} {% if notification_options.followed %}
<a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a> <a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a>
@ -28,13 +29,6 @@
{% else %} {% else %}
<a href=".?mentioned=true"><i class="fa-solid fa-xmark"></i> Mentions</a> <a href=".?mentioned=true"><i class="fa-solid fa-xmark"></i> Mentions</a>
{% endif %} {% endif %}
{% if request.user.admin %}
{% if notification_options.identity_created %}
<a href=".?identity_created=false" class="selected"><i class="fa-solid fa-check"></i> New Identities</a>
{% else %}
<a href=".?identity_created=true"><i class="fa-solid fa-xmark"></i> New Identities</a>
{% endif %}
{% endif %}
</div> </div>
{% for event in events %} {% for event in events %}
@ -52,4 +46,5 @@
<a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a> <a class="button" href=".?page={{ page_obj.next_page_number }}" hx-get=".?page={{ page_obj.next_page_number }}" hx-select=".left-column > *:not(.view-options)" hx-target=".pagination" hx-swap="outerHTML" {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}>Next Page</a>
{% endif %} {% endif %}
</div> </div>
</section>
{% endblock %} {% endblock %}

View file

@ -7,6 +7,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<section class="invisible">
{% for ancestor in ancestors reversed %} {% for ancestor in ancestors reversed %}
{% include "activities/_post.html" with post=ancestor reply=True link_original=False %} {% include "activities/_post.html" with post=ancestor reply=True link_original=False %}
{% endfor %} {% endfor %}
@ -14,4 +15,5 @@
{% for descendant in descendants %} {% for descendant in descendants %}
{% include "activities/_post.html" with post=descendant reply=True link_original=False %} {% include "activities/_post.html" with post=descendant reply=True link_original=False %}
{% endfor %} {% endfor %}
</section>
{% endblock %} {% endblock %}

View file

@ -3,10 +3,8 @@
{% block title %}#{{ hashtag.display_name }} Timeline{% endblock %} {% block title %}#{{ hashtag.display_name }} Timeline{% endblock %}
{% block content %} {% block content %}
<section class="invisible">
<div class="timeline-name"> <div class="timeline-name">
<div class="inline follow follow-hashtag">
{% include "activities/_hashtag_follow.html" %}
</div>
<div class="hashtag"> <div class="hashtag">
<i class="fa-solid fa-hashtag"></i>{{ hashtag.display_name }} <i class="fa-solid fa-hashtag"></i>{{ hashtag.display_name }}
</div> </div>
@ -26,4 +24,5 @@
<a class="button" href=".?page={{ page_obj.next_page_number }} {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}">Next Page</a> <a class="button" href=".?page={{ page_obj.next_page_number }} {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}">Next Page</a>
{% endif %} {% endif %}
</div> </div>
</section>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,61 @@
<nav>
{% if request.user.moderator or request.user.admin %}
<h3>Moderation</h3>
<a href="{% url "admin_identities" %}" {% if section == "identities" %}class="selected"{% endif %} title="Identities">
<i class="fa-solid fa-id-card"></i>
<span>Identities</span>
</a>
<a href="{% url "admin_invites" %}" {% if section == "invites" %}class="selected"{% endif %} title="Invites">
<i class="fa-solid fa-envelope"></i>
<span>Invites</span>
</a>
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
<i class="fa-solid fa-hashtag"></i>
<span>Hashtags</span>
</a>
<a href="{% url "admin_emoji" %}" {% if section == "emoji" %}class="selected"{% endif %} title="Emoji">
<i class="fa-solid fa-icons"></i>
<span>Emoji</span>
</a>
<a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
<i class="fa-solid fa-flag"></i>
<span>Reports</span>
</a>
{% endif %}
{% if request.user.admin %}
<hr>
<h3>Administration</h3>
<a href="{% url "admin_basic" %}" {% if section == "basic" %}class="selected"{% endif %} title="Basic">
<i class="fa-solid fa-book"></i>
<span>Basic</span>
</a>
<a href="{% url "admin_policies" %}" {% if section == "policies" %}class="selected"{% endif %} title="Policies">
<i class="fa-solid fa-file-lines"></i>
<span>Policies</span>
</a>
<a href="{% url "admin_announcements" %}" {% if section == "announcements" %}class="selected"{% endif %} title="Announcements">
<i class="fa-solid fa-bullhorn"></i>
<span>Announcements</span>
</a>
<a href="{% url "admin_domains" %}" {% if section == "domains" %}class="selected"{% endif %} title="Domains">
<i class="fa-solid fa-globe"></i>
<span>Domains</span>
</a>
<a href="{% url "admin_federation" %}" {% if section == "federation" %}class="selected"{% endif %} title="Federation">
<i class="fa-solid fa-diagram-project"></i>
<span>Federation</span>
</a>
<a href="{% url "admin_users" %}" {% if section == "users" %}class="selected"{% endif %} title="Users">
<i class="fa-solid fa-users"></i>
<span>Users</span>
</a>
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
<i class="fa-solid fa-clock-rotate-left"></i>
<span>Stator</span>
</a>
<a href="/djadmin" title="Django Admin" class="danger">
<i class="fa-solid fa-gear"></i>
<span>Django Admin</span>
</a>
{% endif %}
</nav>

13
templates/admin/base.html Normal file
View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}{% block subtitle %}{% endblock %} - Administration{% endblock %}
{% block content %}
<div class="settings">
{% include "admin/_menu.html" %}
<div class="settings-content">
{% block settings_content %}
{% endblock %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends "admin/base.html" %}
{% block subtitle %}{{ section.title }}{% endblock %}
{% block settings_content %}
<form action="." method="POST" enctype="multipart/form-data">
{% csrf_token %}
{% for title, fields in fieldsets.items %}
<fieldset>
<legend>{{ title }}</legend>
{% for field in fields %}
{% include "forms/_field.html" %}
{% endfor %}
</fieldset>
{% endfor %}
<div class="buttons">
<button>Save</button>
</div>
</form>
{% endblock %}

View file

@ -1,6 +1,7 @@
<nav> <nav>
{% if identity %} {% if identity %}
{% include "identity/_identity_banner.html" %} {% include "identity/_identity_banner.html" %}
<h3>Settings</h3>
<a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile"> <a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
<i class="fa-solid fa-user"></i> <i class="fa-solid fa-user"></i>
<span>Profile</span> <span>Profile</span>
@ -9,11 +10,21 @@
<i class="fa-solid fa-display"></i> <i class="fa-solid fa-display"></i>
<span>Interface</span> <span>Interface</span>
</a> </a>
<a href="{% url "settings_follows" handle=identity.handle %}" {% if section == "follows" %}class="selected"{% endif %} title="Follows">
<i class="fa-solid fa-arrow-right-arrow-left"></i>
<span>Follows</span>
</a>
<a href="{% url "settings_import_export" handle=identity.handle %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface"> <a href="{% url "settings_import_export" handle=identity.handle %}" {% if section == "importexport" %}class="selected"{% endif %} title="Interface">
<i class="fa-solid fa-cloud-arrow-up"></i> <i class="fa-solid fa-cloud-arrow-up"></i>
<span>Import/Export</span> <span>Import/Export</span>
</a> </a>
<hr> <hr>
<h3>Tools</h3>
<a href="{% url "compose" handle=identity.handle %}" {% if section == "compose" %}class="selected"{% endif %} title="Compose">
<i class="fa-solid fa-pen-to-square"></i>
<span>Compose</span>
</a>
<hr>
{% endif %} {% endif %}
<h3>Account</h3> <h3>Account</h3>
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login &amp; Security"> <a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login &amp; Security">

View file

@ -0,0 +1,46 @@
{% extends "settings/base.html" %}
{% block subtitle %}Follows{% endblock %}
{% block settings_content %}
<div class="view-options">
{% if inbound %}
<a href=".">Following ({{ num_outbound }})</a>
<a href="." class="selected">Followers ({{ num_inbound }})</a>
{% else %}
<a href=".?inbound=true" class="selected">Following ({{ num_outbound }})</a>
<a href=".?inbound=true">Followers ({{ num_inbound }})</a>
{% endif %}
</div>
<table class="items">
{% for other_identity in page_obj %}
<tr>
<td class="icon">
<a href="{{ domain.urls.edit }}" class="overlay"></a>
<img
src="{{ other_identity.local_icon_url.relative }}"
class="icon"
alt="Avatar for {{ other_identity.name_or_handle }}"
loading="lazy"
data-handle="{{ other_identity.name_or_handle }}"
_="on error set my.src to generate_avatar(@data-handle)"
>
</td>
<td>
<a href="{{ other_identity.urls.view }}" class="overlay">{{ other_identity.handle }}</a>
</td>
<td class="stat">
{% if identity.id in outbound_ids %}
<span class="pill">Following</span>
{% endif %}
{% if identity.id in inbound_ids %}
<span class="pill">Follows You</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr class="empty"><td>You {% if inbound %}have no followers{% else %}are not following anyone{% endif %}.</td></tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -32,7 +32,7 @@
<small>{{ numbers.outbound_follows }} {{ numbers.outbound_follows|pluralize:"follow,follows" }}</small> <small>{{ numbers.outbound_follows }} {{ numbers.outbound_follows|pluralize:"follow,follows" }}</small>
</td> </td>
<td> <td>
<a href="{% url "settings_export_following_csv" %}">Download CSV</a> <a href="{% url "settings_export_following_csv" handle=identity.handle %}">Download CSV</a>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -41,7 +41,7 @@
<small>{{ numbers.inbound_follows }} {{ numbers.inbound_follows|pluralize:"follower,followers" }}</small> <small>{{ numbers.inbound_follows }} {{ numbers.inbound_follows|pluralize:"follower,followers" }}</small>
</td> </td>
<td> <td>
<a href="{% url "settings_export_followers_csv" %}">Download CSV</a> <a href="{% url "settings_export_followers_csv" handle=identity.handle %}">Download CSV</a>
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -22,7 +22,6 @@
{% include "forms/_field.html" with field=form.image %} {% include "forms/_field.html" with field=form.image %}
</fieldset> </fieldset>
<div class="buttons"> <div class="buttons">
<a href="{{ request.identity.urls.view }}" class="button secondary left">View Profile</a>
<button>Save</button> <button>Save</button>
</div> </div>
</form> </form>

View file

@ -34,3 +34,14 @@ def by_handle_or_404(request, handle, local=True, fetch=False) -> Identity:
if identity.blocked: if identity.blocked:
raise Http404("Blocked user") raise Http404("Blocked user")
return identity return identity
def by_handle_for_user_or_404(request, handle):
"""
Retrieves an identity the local user can control via their handle, or
raises a 404.
"""
identity = by_handle_or_404(request, handle, local=True, fetch=False)
if not identity.users.filter(id=request.user.id).exists():
raise Http404("Current user does not own identity")
return identity

View file

@ -57,12 +57,12 @@ class ReportView(FormView):
if "valid" in request.POST: if "valid" in request.POST:
self.report.resolved = timezone.now() self.report.resolved = timezone.now()
self.report.valid = True self.report.valid = True
self.report.moderator = self.request.identity self.report.moderator = self.request.user.identities.all()[0]
self.report.save() self.report.save()
if "invalid" in request.POST: if "invalid" in request.POST:
self.report.resolved = timezone.now() self.report.resolved = timezone.now()
self.report.valid = False self.report.valid = False
self.report.moderator = self.request.identity self.report.moderator = self.request.user.identities.all()[0]
self.report.save() self.report.save()
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)

24
users/views/base.py Normal file
View file

@ -0,0 +1,24 @@
from users.shortcuts import by_handle_for_user_or_404
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
@method_decorator(login_required, name="dispatch")
class IdentityViewMixin:
"""
A mixin that requires that the view has a "handle" kwarg that resolves
to a valid identity that the current user has.
"""
def dispatch(self, request, *args, **kwargs):
self.identity = by_handle_for_user_or_404(request, kwargs["handle"])
self.post_identity_setup()
return super().dispatch(request, *args, **kwargs)
def post_identity_setup(self):
pass
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["identity"] = self.identity
return context

View file

@ -234,33 +234,6 @@ class IdentityFollows(ListView):
return context return context
@method_decorator(login_required, name="dispatch")
class ActionIdentity(View):
def post(self, request, handle):
identity = by_handle_or_404(self.request, handle, local=False)
# See what action we should perform
action = self.request.POST["action"]
if action == "follow":
IdentityService(request.identity).follow(identity)
elif action == "unfollow":
IdentityService(request.identity).unfollow(identity)
elif action == "block":
IdentityService(request.identity).block(identity)
elif action == "unblock":
IdentityService(request.identity).unblock(identity)
elif action == "mute":
IdentityService(request.identity).mute(identity)
elif action == "unmute":
IdentityService(request.identity).unmute(identity)
elif action == "hide_boosts":
IdentityService(request.identity).follow(identity, boosts=False)
elif action == "show_boosts":
IdentityService(request.identity).follow(identity, boosts=True)
else:
raise ValueError(f"Cannot handle identity action {action}")
return redirect(identity.urls.view)
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
class CreateIdentity(FormView): class CreateIdentity(FormView):
template_name = "identity/create.html" template_name = "identity/create.html"

View file

@ -1,76 +0,0 @@
from django import forms
from django.shortcuts import get_object_or_404, render
from django.utils.decorators import method_decorator
from django.views.generic import FormView
from django.contrib.auth.decorators import login_required
from users.models import Report
from users.shortcuts import by_handle_or_404
@method_decorator(login_required, name="dispatch")
class SubmitReport(FormView):
"""
Submits a report on a user or a post
"""
template_name = "users/report.html"
class form_class(forms.Form):
type = forms.ChoiceField(
choices=[
("", "------"),
("spam", "Spam or inappropriate advertising"),
("hateful", "Hateful, abusive, or violent speech"),
("other", "Something else"),
],
label="Why are you reporting this?",
)
complaint = forms.CharField(
widget=forms.Textarea,
help_text="Please describe why you think this should be removed",
)
forward = forms.BooleanField(
widget=forms.Select(
choices=[
(False, "Do not send to other server"),
(True, "Send to other server"),
]
),
help_text="Should we also send an anonymous copy of this to their server?",
required=False,
)
def dispatch(self, request, handle, post_id=None):
self.identity = by_handle_or_404(self.request, handle, local=False)
if post_id:
self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
else:
self.post_obj = None
return super().dispatch(request)
def form_valid(self, form):
# Create the report
report = Report.objects.create(
type=form.cleaned_data["type"],
complaint=form.cleaned_data["complaint"],
subject_identity=self.identity,
subject_post=self.post_obj,
source_identity=self.request.identity,
source_domain=self.request.identity.domain,
forward=form.cleaned_data.get("forward", False),
)
# Show a thanks page
return render(
self.request,
"users/report_sent.html",
{"report": report},
)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["identity"] = self.identity
context["post"] = self.post_obj
return context

View file

@ -12,6 +12,7 @@ from users.views.settings.interface import InterfacePage # noqa
from users.views.settings.profile import ProfilePage # noqa from users.views.settings.profile import ProfilePage # noqa
from users.views.settings.security import SecurityPage # noqa from users.views.settings.security import SecurityPage # noqa
from users.views.settings.settings_page import SettingsPage # noqa from users.views.settings.settings_page import SettingsPage # noqa
from users.views.settings.follows import FollowsPage # noqa
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")

View file

@ -4,15 +4,15 @@ from django.views.generic import ListView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from users.models import Follow, FollowStates, IdentityStates from users.models import Follow, FollowStates, IdentityStates
from users.views.base import IdentityViewMixin
@method_decorator(login_required, name="dispatch") class FollowsPage(IdentityViewMixin, ListView):
class Follows(ListView):
""" """
Shows followers/follows. Shows followers/follows.
""" """
template_name = "activities/follows.html" template_name = "settings/follows.html"
extra_context = { extra_context = {
"section": "follows", "section": "follows",
} }
@ -24,9 +24,9 @@ class Follows(ListView):
def get_queryset(self): def get_queryset(self):
if self.inbound: if self.inbound:
follow_dir = models.Q(target=self.request.identity) follow_dir = models.Q(target=self.identity)
else: else:
follow_dir = models.Q(source=self.request.identity) follow_dir = models.Q(source=self.identity)
return ( return (
Follow.objects.filter( Follow.objects.filter(
@ -65,7 +65,7 @@ class Follows(ListView):
) )
identity_ids = [identity.id for identity in context["page_obj"]] identity_ids = [identity.id for identity in context["page_obj"]]
context["outbound_ids"] = Follow.objects.filter( context["outbound_ids"] = Follow.objects.filter(
source=self.request.identity, source=self.identity,
target_id__in=identity_ids, target_id__in=identity_ids,
state__in=FollowStates.group_active(), state__in=FollowStates.group_active(),
).values_list("target_id", flat=True) ).values_list("target_id", flat=True)
@ -75,17 +75,18 @@ class Follows(ListView):
) )
identity_ids = [identity.id for identity in context["page_obj"]] identity_ids = [identity.id for identity in context["page_obj"]]
context["inbound_ids"] = Follow.objects.filter( context["inbound_ids"] = Follow.objects.filter(
target=self.request.identity, target=self.identity,
source_id__in=identity_ids, source_id__in=identity_ids,
state__in=FollowStates.group_active(), state__in=FollowStates.group_active(),
).values_list("source_id", flat=True) ).values_list("source_id", flat=True)
context["inbound"] = self.inbound context["inbound"] = self.inbound
context["num_inbound"] = Follow.objects.filter( context["num_inbound"] = Follow.objects.filter(
target=self.request.identity, target=self.identity,
state__in=FollowStates.group_active(), state__in=FollowStates.group_active(),
).count() ).count()
context["num_outbound"] = Follow.objects.filter( context["num_outbound"] = Follow.objects.filter(
source=self.request.identity, source=self.identity,
state__in=FollowStates.group_active(), state__in=FollowStates.group_active(),
).count() ).count()
context["identity"] = self.identity
return context return context

View file

@ -1,6 +1,7 @@
import csv import csv
from typing import Any
from django import forms from django import forms, http
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -8,10 +9,11 @@ from django.views.generic import FormView, View
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from users.models import Follow, InboxMessage from users.models import Follow, InboxMessage
from users.views.base import IdentityViewMixin
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
class ImportExportPage(FormView): class ImportExportPage(IdentityViewMixin, FormView):
""" """
Lets the identity's profile be edited Lets the identity's profile be edited
""" """
@ -48,7 +50,7 @@ class ImportExportPage(FormView):
InboxMessage.create_internal( InboxMessage.create_internal(
{ {
"type": "AddFollow", "type": "AddFollow",
"source": self.request.identity.pk, "source": self.identity.pk,
"target_handle": entry["handle"], "target_handle": entry["handle"],
"boosts": entry["boosts"], "boosts": entry["boosts"],
} }
@ -58,14 +60,10 @@ class ImportExportPage(FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["numbers"] = { context["numbers"] = {
"outbound_follows": self.request.identity.outbound_follows.active().count(), "outbound_follows": self.identity.outbound_follows.active().count(),
"inbound_follows": self.request.identity.inbound_follows.active().count(), "inbound_follows": self.identity.inbound_follows.active().count(),
"blocks": self.request.identity.outbound_blocks.active() "blocks": self.identity.outbound_blocks.active().filter(mute=False).count(),
.filter(mute=False) "mutes": self.identity.outbound_blocks.active().filter(mute=True).count(),
.count(),
"mutes": self.request.identity.outbound_blocks.active()
.filter(mute=True)
.count(),
} }
context["bad_format"] = self.request.GET.get("bad_format") context["bad_format"] = self.request.GET.get("bad_format")
context["success"] = self.request.GET.get("success") context["success"] = self.request.GET.get("success")
@ -115,7 +113,7 @@ class CsvView(View):
return response return response
class CsvFollowing(CsvView): class CsvFollowing(IdentityViewMixin, CsvView):
columns = { columns = {
"Account address": "get_handle", "Account address": "get_handle",
"Show boosts": "boosts", "Show boosts": "boosts",
@ -126,7 +124,7 @@ class CsvFollowing(CsvView):
filename = "following.csv" filename = "following.csv"
def get_queryset(self, request): def get_queryset(self, request):
return self.request.identity.outbound_follows.active() return self.identity.outbound_follows.active()
def get_handle(self, follow: Follow): def get_handle(self, follow: Follow):
return follow.target.handle return follow.target.handle
@ -138,7 +136,7 @@ class CsvFollowing(CsvView):
return "" return ""
class CsvFollowers(CsvView): class CsvFollowers(IdentityViewMixin, CsvView):
columns = { columns = {
"Account address": "get_handle", "Account address": "get_handle",
} }
@ -146,7 +144,7 @@ class CsvFollowers(CsvView):
filename = "followers.csv" filename = "followers.csv"
def get_queryset(self, request): def get_queryset(self, request):
return self.request.identity.inbound_follows.active() return self.identity.inbound_follows.active()
def get_handle(self, follow: Follow): def get_handle(self, follow: Follow):
return follow.source.handle return follow.source.handle