mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-22 15:21:01 +00:00
Continue to refactor
This commit is contained in:
parent
5d68e3aaac
commit
0225c6e8ba
39 changed files with 364 additions and 603 deletions
|
@ -72,7 +72,12 @@ class PostService:
|
|||
def unboost_as(self, identity: Identity):
|
||||
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.
|
||||
|
||||
|
@ -82,7 +87,6 @@ class PostService:
|
|||
If identity is provided, includes mentions/followers-only posts they
|
||||
can see. Otherwise, shows unlisted and above only.
|
||||
"""
|
||||
num_ancestors = 10
|
||||
num_descendants = 50
|
||||
# Retrieve ancestors via parent walk
|
||||
ancestors: list[Post] = []
|
||||
|
|
|
@ -16,7 +16,7 @@ from activities.models import (
|
|||
from core.files import blurhash_image, resize_image
|
||||
from core.html import FediverseHtmlParser
|
||||
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
|
||||
|
||||
|
||||
|
@ -167,10 +167,10 @@ class Compose(FormView):
|
|||
)
|
||||
# Add their own timeline event for immediate visibility
|
||||
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):
|
||||
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
|
||||
if handle and 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["reply_to"] = self.reply_to
|
||||
context["identity"] = self.identity
|
||||
context["section"] = "compose"
|
||||
if self.post_obj:
|
||||
context["post"] = self.post_obj
|
||||
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}
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -47,7 +47,9 @@ class Individual(TemplateView):
|
|||
def get_context_data(self, **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(
|
||||
{
|
||||
|
@ -69,27 +71,3 @@ class Individual(TemplateView):
|
|||
canonicalise(self.post_obj.to_ap(), include_security=True),
|
||||
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("/")
|
||||
|
|
|
@ -15,7 +15,7 @@ class Search(FormView):
|
|||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
searcher = SearchService(form.cleaned_data["query"], self.request.identity)
|
||||
searcher = SearchService(form.cleaned_data["query"], identity=None)
|
||||
# Render results
|
||||
context = self.get_context_data(form=form)
|
||||
context["results"] = searcher.search_all()
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Any
|
||||
from django.http import Http404
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -8,6 +10,7 @@ from activities.services import TimelineService
|
|||
from core.decorators import cache_page
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from users.models import Bookmark, HashtagFollow, Identity
|
||||
from users.views.base import IdentityViewMixin
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -46,74 +49,15 @@ class Tag(ListView):
|
|||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return TimelineService(self.request.identity).hashtag(self.hashtag)
|
||||
return TimelineService(None).hashtag(self.hashtag)
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
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
|
||||
|
||||
|
||||
@method_decorator(
|
||||
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):
|
||||
class Notifications(IdentityViewMixin, ListView):
|
||||
template_name = "activities/notifications.html"
|
||||
extra_context = {
|
||||
"current_page": "notifications",
|
||||
|
@ -125,7 +69,6 @@ class Notifications(ListView):
|
|||
"boosted": TimelineEvent.Types.boosted,
|
||||
"mentioned": TimelineEvent.Types.mentioned,
|
||||
"liked": TimelineEvent.Types.liked,
|
||||
"identity_created": TimelineEvent.Types.identity_created,
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -143,7 +86,7 @@ class Notifications(ListView):
|
|||
for type_name, type in self.notification_types.items():
|
||||
if notification_options.get(type_name, True):
|
||||
types.append(type)
|
||||
return TimelineService(self.request.identity).notifications(types)
|
||||
return TimelineService(self.identity).notifications(types)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -164,12 +107,6 @@ class Notifications(ListView):
|
|||
events.append(event)
|
||||
# Retrieve what kinds of things to show
|
||||
context["events"] = events
|
||||
context["identity"] = self.identity
|
||||
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
|
||||
|
|
|
@ -20,16 +20,6 @@ def vary_by_ap_json(request, *args, **kwargs) -> str:
|
|||
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(
|
||||
timeout: int | str = "cache_timeout_page_default",
|
||||
*,
|
||||
|
|
|
@ -30,7 +30,7 @@ class About(TemplateView):
|
|||
template_name = "about.html"
|
||||
|
||||
def get_context_data(self):
|
||||
service = TimelineService(self.request.identity)
|
||||
service = TimelineService(None)
|
||||
return {
|
||||
"current_page": "about",
|
||||
"content": mark_safe(
|
||||
|
|
42
static/img/apps/elk.svg
Executable file
42
static/img/apps/elk.svg
Executable 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
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
BIN
static/img/apps/tusky.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -5,9 +5,6 @@ from django.urls import include, path, re_path
|
|||
from activities.views import (
|
||||
compose,
|
||||
debug,
|
||||
explore,
|
||||
follows,
|
||||
hashtags,
|
||||
posts,
|
||||
search,
|
||||
timelines,
|
||||
|
@ -22,28 +19,21 @@ from users.views import (
|
|||
announcements,
|
||||
auth,
|
||||
identity,
|
||||
report,
|
||||
settings,
|
||||
)
|
||||
from users.views.settings import follows
|
||||
|
||||
urlpatterns = [
|
||||
path("", core.homepage),
|
||||
path("robots.txt", core.RobotsTxt.as_view()),
|
||||
# Activity views
|
||||
path("notifications/", timelines.Notifications.as_view(), name="notifications"),
|
||||
path("local/", timelines.Local.as_view(), name="local"),
|
||||
path("federated/", timelines.Federated.as_view(), name="federated"),
|
||||
path(
|
||||
"@<handle>/notifications/",
|
||||
timelines.Notifications.as_view(),
|
||||
name="notifications",
|
||||
),
|
||||
path("search/", search.Search.as_view(), name="search"),
|
||||
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
|
||||
path(
|
||||
"settings/",
|
||||
|
@ -70,6 +60,11 @@ urlpatterns = [
|
|||
settings.InterfacePage.as_view(),
|
||||
name="settings_interface",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/follows/",
|
||||
settings.FollowsPage.as_view(),
|
||||
name="settings_follows",
|
||||
),
|
||||
path(
|
||||
"@<handle>/settings/import_export/",
|
||||
settings.ImportExportPage.as_view(),
|
||||
|
@ -240,22 +235,12 @@ urlpatterns = [
|
|||
path("@<handle>/", identity.ViewIdentity.as_view()),
|
||||
path("@<handle>/inbox/", activitypub.Inbox.as_view()),
|
||||
path("@<handle>/outbox/", activitypub.Outbox.as_view()),
|
||||
path("@<handle>/action/", identity.ActionIdentity.as_view()),
|
||||
path("@<handle>/rss/", identity.IdentityFeed()),
|
||||
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
|
||||
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>/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
|
||||
path("auth/login/", auth.Login.as_view(), name="login"),
|
||||
path("auth/logout/", auth.Logout.as_view(), name="logout"),
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -76,15 +76,15 @@
|
|||
<div class="actions">
|
||||
<a title="Replies">
|
||||
<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 title="Likes">
|
||||
<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 title="Boosts">
|
||||
<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 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>
|
||||
|
@ -96,14 +96,7 @@
|
|||
<a href="{{ post.urls.action_report }}" role="menuitem">
|
||||
<i class="fa-solid fa-flag"></i> Report
|
||||
</a>
|
||||
{% if post.author == request.identity %}
|
||||
<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 %}
|
||||
{% if not post.local and post.url %}
|
||||
<a href="{{ post.url }}" role="menuitem">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i> See Original
|
||||
</a>
|
||||
|
|
|
@ -1,41 +1,26 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block title %}Compose{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
<form action="." method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Content</legend>
|
||||
<legend>Compose</legend>
|
||||
{% if reply_to %}
|
||||
<label>Replying to</label>
|
||||
{% include "activities/_mini_post.html" with post=reply_to %}
|
||||
{% endif %}
|
||||
<p><i>For more advanced posting options, like editing and image uploads, please use an app.</i></p>
|
||||
{{ form.reply_to }}
|
||||
{{ form.id }}
|
||||
{% include "forms/_field.html" with field=form.text %}
|
||||
{% include "forms/_field.html" with field=form.content_warning %}
|
||||
{% include "forms/_field.html" with field=form.visibility %}
|
||||
</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">
|
||||
<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>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{% extends "activities/local.html" %}
|
||||
|
||||
{% block title %}Federated Timeline{% endblock %}
|
|
@ -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 %}
|
|
@ -1,55 +1,50 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "settings/base.html" %}
|
||||
|
||||
{% block title %}Notifications{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block settings_content %}
|
||||
{% if page_obj.number == 1 %}
|
||||
{% include "_announcements.html" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="view-options">
|
||||
{% if notification_options.followed %}
|
||||
<a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a>
|
||||
{% else %}
|
||||
<a href=".?followed=true"><i class="fa-solid fa-xmark"></i> Followers</a>
|
||||
{% endif %}
|
||||
{% if notification_options.boosted %}
|
||||
<a href=".?boosted=false" class="selected"><i class="fa-solid fa-check"></i> Boosts</a>
|
||||
{% else %}
|
||||
<a href=".?boosted=true"><i class="fa-solid fa-xmark"></i> Boosts</a>
|
||||
{% endif %}
|
||||
{% if notification_options.liked %}
|
||||
<a href=".?liked=false" class="selected"><i class="fa-solid fa-check"></i> Likes</a>
|
||||
{% else %}
|
||||
<a href=".?liked=true"><i class="fa-solid fa-xmark"></i> Likes</a>
|
||||
{% endif %}
|
||||
{% if notification_options.mentioned %}
|
||||
<a href=".?mentioned=false" class="selected"><i class="fa-solid fa-check"></i> Mentions</a>
|
||||
{% else %}
|
||||
<a href=".?mentioned=true"><i class="fa-solid fa-xmark"></i> Mentions</a>
|
||||
{% 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>
|
||||
<section class="invisible">
|
||||
<div class="view-options">
|
||||
{% if notification_options.followed %}
|
||||
<a href=".?followed=false" class="selected"><i class="fa-solid fa-check"></i> Followers</a>
|
||||
{% else %}
|
||||
<a href=".?identity_created=true"><i class="fa-solid fa-xmark"></i> New Identities</a>
|
||||
<a href=".?followed=true"><i class="fa-solid fa-xmark"></i> Followers</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if notification_options.boosted %}
|
||||
<a href=".?boosted=false" class="selected"><i class="fa-solid fa-check"></i> Boosts</a>
|
||||
{% else %}
|
||||
<a href=".?boosted=true"><i class="fa-solid fa-xmark"></i> Boosts</a>
|
||||
{% endif %}
|
||||
{% if notification_options.liked %}
|
||||
<a href=".?liked=false" class="selected"><i class="fa-solid fa-check"></i> Likes</a>
|
||||
{% else %}
|
||||
<a href=".?liked=true"><i class="fa-solid fa-xmark"></i> Likes</a>
|
||||
{% endif %}
|
||||
{% if notification_options.mentioned %}
|
||||
<a href=".?mentioned=false" class="selected"><i class="fa-solid fa-check"></i> Mentions</a>
|
||||
{% else %}
|
||||
<a href=".?mentioned=true"><i class="fa-solid fa-xmark"></i> Mentions</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for event in events %}
|
||||
{% include "activities/_event.html" %}
|
||||
{% empty %}
|
||||
No notifications yet.
|
||||
{% endfor %}
|
||||
{% for event in events %}
|
||||
{% include "activities/_event.html" %}
|
||||
{% empty %}
|
||||
No notifications 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 %}
|
||||
<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>
|
||||
{% 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>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% for ancestor in ancestors reversed %}
|
||||
{% include "activities/_post.html" with post=ancestor reply=True link_original=False %}
|
||||
{% endfor %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% for descendant in descendants %}
|
||||
{% include "activities/_post.html" with post=descendant reply=True link_original=False %}
|
||||
{% endfor %}
|
||||
<section class="invisible">
|
||||
{% for ancestor in ancestors reversed %}
|
||||
{% include "activities/_post.html" with post=ancestor reply=True link_original=False %}
|
||||
{% endfor %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% for descendant in descendants %}
|
||||
{% include "activities/_post.html" with post=descendant reply=True link_original=False %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,27 +3,26 @@
|
|||
{% block title %}#{{ hashtag.display_name }} Timeline{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="timeline-name">
|
||||
<div class="inline follow follow-hashtag">
|
||||
{% include "activities/_hashtag_follow.html" %}
|
||||
<section class="invisible">
|
||||
<div class="timeline-name">
|
||||
<div class="hashtag">
|
||||
<i class="fa-solid fa-hashtag"></i>{{ hashtag.display_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hashtag">
|
||||
<i class="fa-solid fa-hashtag"></i>{{ hashtag.display_name }}
|
||||
{% for post in page_obj %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% empty %}
|
||||
No posts yet.
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% 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 }} {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% for post in page_obj %}
|
||||
{% include "activities/_post.html" %}
|
||||
{% empty %}
|
||||
No posts yet.
|
||||
{% endfor %}
|
||||
|
||||
<div class="pagination">
|
||||
{% 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 }} {% if config_identity.infinite_scroll %}hx-trigger="revealed"{% endif %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
61
templates/admin/_menu.html
Normal file
61
templates/admin/_menu.html
Normal 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
13
templates/admin/base.html
Normal 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 %}
|
20
templates/admin/settings.html
Normal file
20
templates/admin/settings.html
Normal 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 %}
|
|
@ -1,6 +1,7 @@
|
|||
<nav>
|
||||
{% if identity %}
|
||||
{% include "identity/_identity_banner.html" %}
|
||||
<h3>Settings</h3>
|
||||
<a href="{% url "settings_profile" handle=identity.handle %}" {% if section == "profile" %}class="selected"{% endif %} title="Profile">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>Profile</span>
|
||||
|
@ -9,11 +10,21 @@
|
|||
<i class="fa-solid fa-display"></i>
|
||||
<span>Interface</span>
|
||||
</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">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Import/Export</span>
|
||||
</a>
|
||||
<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 %}
|
||||
<h3>Account</h3>
|
||||
<a href="{% url "settings_security" %}" {% if section == "security" %}class="selected"{% endif %} title="Login & Security">
|
||||
|
|
46
templates/settings/follows.html
Normal file
46
templates/settings/follows.html
Normal 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 %}
|
|
@ -32,7 +32,7 @@
|
|||
<small>{{ numbers.outbound_follows }} {{ numbers.outbound_follows|pluralize:"follow,follows" }}</small>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -41,7 +41,7 @@
|
|||
<small>{{ numbers.inbound_follows }} {{ numbers.inbound_follows|pluralize:"follower,followers" }}</small>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
{% include "forms/_field.html" with field=form.image %}
|
||||
</fieldset>
|
||||
<div class="buttons">
|
||||
<a href="{{ request.identity.urls.view }}" class="button secondary left">View Profile</a>
|
||||
<button>Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -34,3 +34,14 @@ def by_handle_or_404(request, handle, local=True, fetch=False) -> Identity:
|
|||
if identity.blocked:
|
||||
raise Http404("Blocked user")
|
||||
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
|
||||
|
|
|
@ -57,12 +57,12 @@ class ReportView(FormView):
|
|||
if "valid" in request.POST:
|
||||
self.report.resolved = timezone.now()
|
||||
self.report.valid = True
|
||||
self.report.moderator = self.request.identity
|
||||
self.report.moderator = self.request.user.identities.all()[0]
|
||||
self.report.save()
|
||||
if "invalid" in request.POST:
|
||||
self.report.resolved = timezone.now()
|
||||
self.report.valid = False
|
||||
self.report.moderator = self.request.identity
|
||||
self.report.moderator = self.request.user.identities.all()[0]
|
||||
self.report.save()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
|
24
users/views/base.py
Normal file
24
users/views/base.py
Normal 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
|
|
@ -234,33 +234,6 @@ class IdentityFollows(ListView):
|
|||
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")
|
||||
class CreateIdentity(FormView):
|
||||
template_name = "identity/create.html"
|
||||
|
|
|
@ -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
|
|
@ -12,6 +12,7 @@ from users.views.settings.interface import InterfacePage # noqa
|
|||
from users.views.settings.profile import ProfilePage # noqa
|
||||
from users.views.settings.security import SecurityPage # noqa
|
||||
from users.views.settings.settings_page import SettingsPage # noqa
|
||||
from users.views.settings.follows import FollowsPage # noqa
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
|
|
@ -4,15 +4,15 @@ from django.views.generic import ListView
|
|||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from users.models import Follow, FollowStates, IdentityStates
|
||||
from users.views.base import IdentityViewMixin
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Follows(ListView):
|
||||
class FollowsPage(IdentityViewMixin, ListView):
|
||||
"""
|
||||
Shows followers/follows.
|
||||
"""
|
||||
|
||||
template_name = "activities/follows.html"
|
||||
template_name = "settings/follows.html"
|
||||
extra_context = {
|
||||
"section": "follows",
|
||||
}
|
||||
|
@ -24,9 +24,9 @@ class Follows(ListView):
|
|||
|
||||
def get_queryset(self):
|
||||
if self.inbound:
|
||||
follow_dir = models.Q(target=self.request.identity)
|
||||
follow_dir = models.Q(target=self.identity)
|
||||
else:
|
||||
follow_dir = models.Q(source=self.request.identity)
|
||||
follow_dir = models.Q(source=self.identity)
|
||||
|
||||
return (
|
||||
Follow.objects.filter(
|
||||
|
@ -65,7 +65,7 @@ class Follows(ListView):
|
|||
)
|
||||
identity_ids = [identity.id for identity in context["page_obj"]]
|
||||
context["outbound_ids"] = Follow.objects.filter(
|
||||
source=self.request.identity,
|
||||
source=self.identity,
|
||||
target_id__in=identity_ids,
|
||||
state__in=FollowStates.group_active(),
|
||||
).values_list("target_id", flat=True)
|
||||
|
@ -75,17 +75,18 @@ class Follows(ListView):
|
|||
)
|
||||
identity_ids = [identity.id for identity in context["page_obj"]]
|
||||
context["inbound_ids"] = Follow.objects.filter(
|
||||
target=self.request.identity,
|
||||
target=self.identity,
|
||||
source_id__in=identity_ids,
|
||||
state__in=FollowStates.group_active(),
|
||||
).values_list("source_id", flat=True)
|
||||
context["inbound"] = self.inbound
|
||||
context["num_inbound"] = Follow.objects.filter(
|
||||
target=self.request.identity,
|
||||
target=self.identity,
|
||||
state__in=FollowStates.group_active(),
|
||||
).count()
|
||||
context["num_outbound"] = Follow.objects.filter(
|
||||
source=self.request.identity,
|
||||
source=self.identity,
|
||||
state__in=FollowStates.group_active(),
|
||||
).count()
|
||||
context["identity"] = self.identity
|
||||
return context
|
|
@ -1,6 +1,7 @@
|
|||
import csv
|
||||
from typing import Any
|
||||
|
||||
from django import forms
|
||||
from django import forms, http
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
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 users.models import Follow, InboxMessage
|
||||
from users.views.base import IdentityViewMixin
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ImportExportPage(FormView):
|
||||
class ImportExportPage(IdentityViewMixin, FormView):
|
||||
"""
|
||||
Lets the identity's profile be edited
|
||||
"""
|
||||
|
@ -48,7 +50,7 @@ class ImportExportPage(FormView):
|
|||
InboxMessage.create_internal(
|
||||
{
|
||||
"type": "AddFollow",
|
||||
"source": self.request.identity.pk,
|
||||
"source": self.identity.pk,
|
||||
"target_handle": entry["handle"],
|
||||
"boosts": entry["boosts"],
|
||||
}
|
||||
|
@ -58,14 +60,10 @@ class ImportExportPage(FormView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["numbers"] = {
|
||||
"outbound_follows": self.request.identity.outbound_follows.active().count(),
|
||||
"inbound_follows": self.request.identity.inbound_follows.active().count(),
|
||||
"blocks": self.request.identity.outbound_blocks.active()
|
||||
.filter(mute=False)
|
||||
.count(),
|
||||
"mutes": self.request.identity.outbound_blocks.active()
|
||||
.filter(mute=True)
|
||||
.count(),
|
||||
"outbound_follows": self.identity.outbound_follows.active().count(),
|
||||
"inbound_follows": self.identity.inbound_follows.active().count(),
|
||||
"blocks": self.identity.outbound_blocks.active().filter(mute=False).count(),
|
||||
"mutes": self.identity.outbound_blocks.active().filter(mute=True).count(),
|
||||
}
|
||||
context["bad_format"] = self.request.GET.get("bad_format")
|
||||
context["success"] = self.request.GET.get("success")
|
||||
|
@ -115,7 +113,7 @@ class CsvView(View):
|
|||
return response
|
||||
|
||||
|
||||
class CsvFollowing(CsvView):
|
||||
class CsvFollowing(IdentityViewMixin, CsvView):
|
||||
columns = {
|
||||
"Account address": "get_handle",
|
||||
"Show boosts": "boosts",
|
||||
|
@ -126,7 +124,7 @@ class CsvFollowing(CsvView):
|
|||
filename = "following.csv"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return self.request.identity.outbound_follows.active()
|
||||
return self.identity.outbound_follows.active()
|
||||
|
||||
def get_handle(self, follow: Follow):
|
||||
return follow.target.handle
|
||||
|
@ -138,7 +136,7 @@ class CsvFollowing(CsvView):
|
|||
return ""
|
||||
|
||||
|
||||
class CsvFollowers(CsvView):
|
||||
class CsvFollowers(IdentityViewMixin, CsvView):
|
||||
columns = {
|
||||
"Account address": "get_handle",
|
||||
}
|
||||
|
@ -146,7 +144,7 @@ class CsvFollowers(CsvView):
|
|||
filename = "followers.csv"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return self.request.identity.inbound_follows.active()
|
||||
return self.identity.inbound_follows.active()
|
||||
|
||||
def get_handle(self, follow: Follow):
|
||||
return follow.source.handle
|
||||
|
|
Loading…
Reference in a new issue