diff --git a/activities/models/emoji.py b/activities/models/emoji.py index dce862c..61a0cd8 100644 --- a/activities/models/emoji.py +++ b/activities/models/emoji.py @@ -125,10 +125,12 @@ class Emoji(StatorModel): unique_together = ("domain", "shortcode") class urls(urlman.Urls): - root = "/admin/emoji/" - create = "{root}/create/" - edit = "{root}{self.Emoji}/" - delete = "{edit}delete/" + admin = "/admin/emoji/" + admin_create = "{admin}create/" + admin_edit = "{admin}{self.pk}/" + admin_delete = "{admin}{self.pk}/delete/" + admin_enable = "{admin}{self.pk}/enable/" + admin_disable = "{admin}{self.pk}/disable/" emoji_regex = re.compile(r"\B:([a-zA-Z0-9(_)-]+):\B") @@ -172,8 +174,11 @@ class Emoji(StatorModel): self.public is None and Config.system.emoji_unreviewed_are_public ) - def full_url(self) -> RelativeAbsoluteUrl: - if self.is_usable: + def full_url_admin(self) -> RelativeAbsoluteUrl: + return self.full_url(always_show=True) + + def full_url(self, always_show=False) -> RelativeAbsoluteUrl: + if self.is_usable or always_show: if self.file: return AutoAbsoluteUrl(self.file.url) elif self.remote_url: diff --git a/activities/models/hashtag.py b/activities/models/hashtag.py index 7d34892..8430fd4 100644 --- a/activities/models/hashtag.py +++ b/activities/models/hashtag.py @@ -117,6 +117,8 @@ class Hashtag(StatorModel): view = "/tags/{self.hashtag}/" admin = "/admin/hashtags/" admin_edit = "{admin}{self.hashtag}/" + admin_enable = "{admin_edit}enable/" + admin_disable = "{admin_edit}disable/" timeline = "/tags/{self.hashtag}/" hashtag_regex = re.compile(r"\B#([a-zA-Z0-9(_)]+\b)(?!;)") diff --git a/activities/templatetags/activity_tags.py b/activities/templatetags/activity_tags.py index 571e2d6..5280856 100644 --- a/activities/templatetags/activity_tags.py +++ b/activities/templatetags/activity_tags.py @@ -1,4 +1,5 @@ import datetime +from urllib.parse import urlencode from django import template from django.utils import timezone @@ -31,3 +32,18 @@ def timedeltashort(value: datetime.datetime): years = max(days // 365.25, 1) text = f"{years:0n}y" return text + + +@register.simple_tag(takes_context=True) +def urlparams(context, **kwargs): + """ + Generates a URL parameter string the same as the current page but with + the given items changed. + """ + params = dict(context["request"].GET.items()) + for name, value in kwargs.items(): + if value: + params[name] = value + elif name in params: + del params[name] + return urlencode(params) diff --git a/static/css/style.css b/static/css/style.css index 8be0914..c4de68b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -555,6 +555,92 @@ p.authorization-code { color: var(--color-text-dull); } +/* Item tables */ + +table.items { + margin: 10px 0; + border: 1px solid var(--color-bg-menu); + border-spacing: 0; + border-radius: 5px; + width: 100%; +} + +table.items td { + padding: 10px; + vertical-align: middle; + line-height: 1.1em; + height: 55px; + position: relative; +} + +table.items td small { + display: block; + color: var(--color-text-dull); +} + +table.items tr:nth-of-type(2n+1) { + background-color: var(--color-bg-box); +} + +table.items td.name { + width: 100%; +} + +table.items td.name a.overlay, +table.items td.icon a.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +table.items td.icon { + width: 40px; + padding-left: 20px; +} + +table.items td.icon img { + width: 32px; + display: block; +} + +table.items td .bad { + background: var(--color-delete); + padding: 4px 6px; + border-radius: 3px; +} + +table.items td.stat { + white-space: nowrap; +} + +table.items td.actions { + text-align: right; + white-space: nowrap; +} + +table.items td.actions a { + color: var(--color-text-dull); + padding: 3px 4px; + border-radius: 3px; + text-decoration: none; + border: 3px solid transparent; + margin: 0 0 0 3px; + cursor: pointer; + display: inline-block; + text-align: center; + width: 30px; +} + +table.items td.actions a:hover { + color: var(--color-text-main); + background-color: rgba(255, 255, 255, 0.1); +} + +table.items td.actions a.danger:hover { + background-color: var(--color-delete); +} /* Forms */ @@ -1189,13 +1275,14 @@ table.metadata td .emoji { .view-options { margin: 0 0 10px 0px; + display: flex; } .view-options.follows { margin: 0 0 20px 0px; } -.view-options a { +.view-options a:not(.button) { display: inline-block; margin: 0 10px 5px 0; padding: 4px 12px; @@ -1204,11 +1291,17 @@ table.metadata td .emoji { border-radius: 3px; } -.view-options a:hover { +.view-options a.button { + display: inline-block; + margin: 0 0 5px auto; + padding: 1px 8px; +} + +.view-options a:not(.button):hover { color: var(--color-text-dull); } -.view-options a.selected { +.view-options a:not(.button).selected { color: var(--color-text-highlight); } @@ -1723,7 +1816,8 @@ form .post { z-index: 100; } -#image-viewer picture, #image-viewer img { +#image-viewer picture, +#image-viewer img { display: block; } diff --git a/takahe/urls.py b/takahe/urls.py index 76dd98f..d4e7ff5 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -148,6 +148,23 @@ urlpatterns = [ "admin/hashtags//", admin.HashtagEdit.as_view(), ), + path("admin/hashtags//enable/", admin.HashtagEnable.as_view()), + path( + "admin/hashtags//disable/", admin.HashtagEnable.as_view(enable=False) + ), + path( + "admin/emoji/", + admin.EmojiRoot.as_view(), + name="admin_emoji", + ), + path( + "admin/emoji/create/", + admin.EmojiCreate.as_view(), + name="admin_emoji_create", + ), + path("admin/emoji//enable/", admin.EmojiEnable.as_view()), + path("admin/emoji//disable/", admin.EmojiEnable.as_view(enable=False)), + path("admin/emoji//delete/", admin.EmojiDelete.as_view()), path( "admin/stator/", admin.Stator.as_view(), diff --git a/templates/admin/domain_create.html b/templates/admin/domain_create.html index 23c1ebf..e23a356 100644 --- a/templates/admin/domain_create.html +++ b/templates/admin/domain_create.html @@ -15,14 +15,8 @@ and you will not be able to delete a domain with identities on it.

- If you will be serving Takahē on the domain you choose, you can leave - the "service domain" field blank. If you would like to let users create - accounts on a domain serving something else, you must pick a unique - "service domain" that pairs up to your chosen domain name, make sure - Takahē is served on that, and add redirects - for /.well-known/webfinger, /.well-known/host-meta - and /.well-known/nodeinfo from the main domain to the - service domain. + For more information about domain setup, including what service + domains are, see our documentation on domains.

{% csrf_token %}
diff --git a/templates/admin/domains.html b/templates/admin/domains.html index 8ef09fe..028c344 100644 --- a/templates/admin/domains.html +++ b/templates/admin/domains.html @@ -3,26 +3,31 @@ {% block subtitle %}Domains{% endblock %} {% block content %} -
+ + {% for domain in domains %} - - - + + + + {% empty %} -

You have no domains set up.

+
{% endfor %} - - Add a domain - - +
+ + + + {{ domain.domain }} - {% if domain.public %}Public{% else %}Private{% endif %} - {% if domain.service_domain %}({{ domain.service_domain }}){% endif %} + {% if domain.service_domain %}{{ domain.service_domain }}{% endif %} - {% if domain.default %} - Default - {% endif %} - + + {% if domain.public %}Public{% else %}Private{% endif %} + {% if domain.default %}(Default){% endif %} +
You have no domains set up.
{% endblock %} diff --git a/templates/admin/emoji.html b/templates/admin/emoji.html new file mode 100644 index 0000000..2792988 --- /dev/null +++ b/templates/admin/emoji.html @@ -0,0 +1,67 @@ +{% extends "settings/base.html" %} +{% load activity_tags %} + +{% block subtitle %}Emoji{% endblock %} + +{% block content %} + +
+ {% if local_only %} + Local Only + {% else %} + Local Only + {% endif %} + Add Emoji +
+ + {% for emoji in page_obj %} + + + + + + + {% empty %} + + + + {% endfor %} +
+ + + {{ emoji.shortcode }} + {% if emoji.domain %}{{ emoji.domain }}{% endif %} + + + {% if not emoji.is_usable %} + Disabled + + {% else %} + + {% endif %} + +
+ {% if query %} + No emoji match your query. + {% else %} + There are no emoji yet. + {% endif %} +
+ +{% endblock %} diff --git a/templates/admin/emoji_create.html b/templates/admin/emoji_create.html new file mode 100644 index 0000000..128bfcf --- /dev/null +++ b/templates/admin/emoji_create.html @@ -0,0 +1,18 @@ +{% extends "settings/base.html" %} + +{% block subtitle %}{{ emoji.shortcode }}{% endblock %} + +{% block content %} +
+ {% csrf_token %} +
+ Emoji Details + {% include "forms/_field.html" with field=form.shortcode %} + {% include "forms/_field.html" with field=form.image hide_existing=True %} +
+
+ Back + +
+
+{% endblock %} diff --git a/templates/admin/federation.html b/templates/admin/federation.html index 0f20ada..b176d65 100644 --- a/templates/admin/federation.html +++ b/templates/admin/federation.html @@ -1,4 +1,5 @@ {% extends "settings/base.html" %} +{% load activity_tags %} {% block subtitle %}Federation{% endblock %} @@ -7,33 +8,49 @@ -
+ {% for domain in page_obj %} - - - + + + + + + {% empty %} -

There are no federation links yet.

+
+ + {% endfor %} - - +
+ + + + {{ domain.domain }} - - {{ domain.num_users }} remote identit{{ domain.num_users|pluralize:"y,ies" }} - - - {% if domain.blocked %} - Blocked - {% endif %} - + {{ domain.software }} + + {% if domain.blocked %} + Blocked + {% endif %} + + {{ domain.num_users }} + identit{{ domain.num_users|pluralize:"y,ies" }} +
+ {% if query %} + There are no domains matching your query. + {% else %} + There are no federation links yet. + {% endif %} +
+ {% endblock %} diff --git a/templates/admin/federation_edit.html b/templates/admin/federation_edit.html index f96e7ae..a711f74 100644 --- a/templates/admin/federation_edit.html +++ b/templates/admin/federation_edit.html @@ -11,7 +11,7 @@ {% include "forms/_field.html" with field=form.blocked %}
- Back + Back Delete
diff --git a/templates/admin/hashtags.html b/templates/admin/hashtags.html index 1e9c374..14d5d9a 100644 --- a/templates/admin/hashtags.html +++ b/templates/admin/hashtags.html @@ -3,50 +3,62 @@ {% block subtitle %}Hashtags{% endblock %} {% block content %} -
+ {% for hashtag in page_obj %} - -
+
+ + + + + + {% empty %} -

There are no hashtags yet.

+
+ + {% endfor %} - - +
+ - - {{ hashtag.display_name }} - - {% if hashtag.public %}Public{% elif hashtag.public is None %}Unreviewed{% else %}Private{% endif %} - - - - {% if hashtag.stats %} -
- - {{ hashtag.stats.total }} - Total - -
- {% endif %} - {% if hashtag.aliases %} -
- - {% for alias in hashtag.aliases %} - {{ alias }}{% if not forloop.last %}, {% endif %} - {% endfor %} - Aliases - -
- {% endif %} - +
+ + {{ hashtag.display_name }} + {% if hashtag.public %}Public{% elif hashtag.public is None %}Unreviewed{% else %}Private{% endif %} + + {% if hashtag.stats %} + {{ hashtag.stats.total }} + post{{ hashtag.stats.total|pluralize }} + {% endif %} + + {% if hashtag.aliases %} + {% for alias in hashtag.aliases %} + {{ alias }}{% if not forloop.last %}, {% endif %} + {% endfor %} + Aliases + {% endif %} + + {% if hashtag.public is not True %} + + {% endif %} + {% if hashtag.public is not False %} + + {% endif %} +
+ {% if query %} + No hashtags match your query. + {% else %} + There are no hashtags yet. + {% endif %} +
+ {% endblock %} diff --git a/templates/admin/identities.html b/templates/admin/identities.html index 4614c5e..898cfc4 100644 --- a/templates/admin/identities.html +++ b/templates/admin/identities.html @@ -1,4 +1,5 @@ {% extends "settings/base.html" %} +{% load activity_tags %} {% block subtitle %}Identities{% endblock %} @@ -12,42 +13,71 @@
{% if local_only %} - Local Only + Local Only {% else %} - Local Only + Local Only {% endif %}
-
+ {% for identity in page_obj %} - -
- {% include "identity/_identity_banner.html" with identity=identity link_avatar=False link_handle=False %} -
-
- {% if identity.banned %} - Banned +
+ + + + + + {% empty %} -

- {% if query %} - No identities match your query. - {% else %} - There are no identities yet. - {% endif %} -

+
+ + {% endfor %} - - +
+ + Avatar for {{ identity.name_or_handle }} + + + {{ identity.html_name_or_handle }} + @{{ identity.handle }} + + {% if identity.restriction == 1 %} + Limited + {% elif identity.restriction == 2 %} + Blocked {% endif %} - - + + {% if identity.local %} + Local + {{ identity.followers_count }} follower{{ identity.followers_count|pluralize }} + {% else %} + Remote + {{ identity.followers_count }} local follower{{ identity.followers_count|pluralize }} + {% endif %} + + +
+ {% if query %} + No identities match your query. + {% else %} + There are no identities yet. + {% endif %} +
+ {% endblock %} diff --git a/templates/admin/invites.html b/templates/admin/invites.html index 86d1281..7f26bf0 100644 --- a/templates/admin/invites.html +++ b/templates/admin/invites.html @@ -4,51 +4,62 @@ {% block subtitle %}Invites{% endblock %} {% block content %} -
- -
-
+
+ + Create New +
+ {% for invite in page_obj %} - - - + + + + {% empty %} -

- There are no unused invites. -

+
+ + {% endfor %} - - +
+ + + + {{ invite.token }} - - {% if invite.expires %} - Expires in {{ invite.expires|timeuntil }} - {% if invite.note %}|{% endif %} - {% endif %} {% if invite.note %} {{ invite.note }} {% endif %} - - + {% if invite.expires %} + {% if invite.valid %} + {{ invite.expires|timeuntil }} + until expiry + {% else %} + Expired + {% endif %} {% endif %} - + + {% if invite.uses %} + {{ invite.uses }} + {% else %} + Infinite + {% endif %} + use{{ invite.uses|pluralize }} left + +
+ There are no invites yet. +
+ {% endblock %} diff --git a/templates/admin/reports.html b/templates/admin/reports.html index 0388123..c66bf74 100644 --- a/templates/admin/reports.html +++ b/templates/admin/reports.html @@ -11,36 +11,48 @@ Show Resolved {% endif %} -
+ {% for report in page_obj %} - - Avatar for {{ report.subject_identity.name_or_handle }} - + + + + + + {% empty %} -

- There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}. -

+
+ + {% endfor %} - - +
+ + Avatar for {{ report.subject_identity.name_or_handle }} + + {{ report.subject_identity.html_name_or_handle }} {% if report.subject_post %} - (post {{ report.subject_post.pk }}) + + Post on {{ report.subject_post.published }} + {% endif %} - - {{ report.type|title }} - - - - + + {{ report.type|title }} + Type + + {{ report.created|timedeltashort }} + Reported +
+ There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}. +
+ {% endblock %} diff --git a/templates/admin/users.html b/templates/admin/users.html index c942a39..500d0f2 100644 --- a/templates/admin/users.html +++ b/templates/admin/users.html @@ -1,4 +1,5 @@ {% extends "settings/base.html" %} +{% load activity_tags %} {% block subtitle %}Users{% endblock %} @@ -7,39 +8,51 @@ -
+ {% for user in page_obj %} - - - + + + + + {% empty %} -

- {% if query %} - No users match your query. - {% else %} - There are no users yet. - {% endif %} -

+
+ + {% endfor %} - - +
+ + + + {{ user.email }} + {% if user.admin %}Admin{% elif user.moderator %}Moderator{% endif %} + + {{ user.num_identities }} - {{ user.num_identities }} identit{{ user.num_identities|pluralize:"y,ies" }} + identit{{ user.num_identities|pluralize:"y,ies" }} - {% if user.banned %} - Banned - {% endif %} - + + {% if user.banned %} + Banned + {% endif %} +
+ {% if query %} + No users match your query. + {% else %} + There are no users yet. + {% endif %} +
+ {% endblock %} diff --git a/templates/forms/_field.html b/templates/forms/_field.html index 7a88280..69e0980 100644 --- a/templates/forms/_field.html +++ b/templates/forms/_field.html @@ -10,14 +10,14 @@

{% endif %} {{ field.errors }} - {% if field.field.widget.input_type == "file" and field.value %} + {% if field.field.widget.input_type == "file" and field.value and not hide_existing %}
Clear current value
{% endif %} {{ field }} - {% if field.field.widget.input_type == "file" and field.value %} + {% if field.field.widget.input_type == "file" and field.value and not hide_existing %} {% endif %} diff --git a/templates/settings/_menu.html b/templates/settings/_menu.html index dfdce66..d0aa62f 100644 --- a/templates/settings/_menu.html +++ b/templates/settings/_menu.html @@ -24,6 +24,9 @@ Hashtags + + Emoji + Reports diff --git a/users/models/domain.py b/users/models/domain.py index 11405c9..7dd1a7d 100644 --- a/users/models/domain.py +++ b/users/models/domain.py @@ -203,3 +203,12 @@ class Domain(StatorModel): f"Client error decoding nodeinfo: domain={self.domain}, error={str(ex)}" ) return info + + @property + def software(self): + if self.nodeinfo: + software = self.nodeinfo.get("software", {}) + name = software.get("name", "unknown") + version = software.get("version", "unknown") + return f"{name:.10} - {version:.10}" + return None diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py index 2ce82d2..1a7aee5 100644 --- a/users/views/admin/__init__.py +++ b/users/views/admin/__init__.py @@ -8,8 +8,14 @@ from users.views.admin.domains import ( # noqa DomainEdit, Domains, ) +from users.views.admin.emoji import ( # noqa + EmojiCreate, + EmojiDelete, + EmojiEnable, + EmojiRoot, +) from users.views.admin.federation import FederationEdit, FederationRoot # noqa -from users.views.admin.hashtags import HashtagEdit, Hashtags # noqa +from users.views.admin.hashtags import HashtagEdit, HashtagEnable, Hashtags # noqa from users.views.admin.identities import IdentitiesRoot, IdentityEdit # noqa from users.views.admin.invites import InviteCreate, InvitesRoot, InviteView # noqa from users.views.admin.reports import ReportsRoot, ReportView # noqa diff --git a/users/views/admin/emoji.py b/users/views/admin/emoji.py new file mode 100644 index 0000000..f9c39c9 --- /dev/null +++ b/users/views/admin/emoji.py @@ -0,0 +1,96 @@ +from django import forms +from django.conf import settings +from django.db import models +from django.shortcuts import get_object_or_404, redirect +from django.utils.decorators import method_decorator +from django.views.generic import FormView, ListView, View +from django_htmx.http import HttpResponseClientRefresh + +from activities.models import Emoji +from users.decorators import moderator_required + + +@method_decorator(moderator_required, name="dispatch") +class EmojiRoot(ListView): + + template_name = "admin/emoji.html" + paginate_by = 50 + + def get(self, request, *args, **kwargs): + self.query = request.GET.get("query") + self.local_only = request.GET.get("local_only") + self.extra_context = { + "section": "emoji", + "query": self.query or "", + "local_only": self.local_only, + } + return super().get(request, *args, **kwargs) + + def get_queryset(self): + queryset = Emoji.objects.filter().order_by("shortcode", "domain_id") + if self.local_only: + queryset = queryset.filter(local=True) + if self.query: + query = self.query.lower().strip().lstrip("@") + queryset = queryset.filter( + models.Q(shortcode__icontains=query) | models.Q(domain_id=query) + ) + return queryset + + +@method_decorator(moderator_required, name="dispatch") +class EmojiCreate(FormView): + + template_name = "admin/emoji_create.html" + extra_context = {"section": "emoji"} + + class form_class(forms.Form): + shortcode = forms.SlugField( + help_text="What users type to use the emoji :likethis:", + ) + image = forms.ImageField( + help_text="The emoji image\nShould be at least 40 x 40 pixels, and under 50kb", + ) + + def clean_image(self): + data = self.cleaned_data["image"] + if data.size > settings.SETUP.EMOJI_MAX_IMAGE_FILESIZE_KB: + raise forms.ValidationError("Image filesize is too large") + return data + + def form_valid(self, form): + Emoji.objects.create( + shortcode=form.cleaned_data["shortcode"], + file=form.cleaned_data["image"], + mimetype=form.cleaned_data["image"].image.get_format_mimetype(), + local=True, + public=True, + ) + return redirect(Emoji.urls.admin) + + +@method_decorator(moderator_required, name="dispatch") +class EmojiDelete(View): + """ + Deletes an emoji + """ + + def post(self, request, id): + self.emoji = get_object_or_404(Emoji, pk=id) + self.emoji.delete() + return HttpResponseClientRefresh() + + +@method_decorator(moderator_required, name="dispatch") +class EmojiEnable(View): + """ + Sets an emoji to be enabled (or not!) + """ + + enable = True + + def post(self, request, id): + self.emoji = get_object_or_404(Emoji, pk=id) + self.emoji.public = self.enable + self.emoji.save() + return HttpResponseClientRefresh() diff --git a/users/views/admin/hashtags.py b/users/views/admin/hashtags.py index 3bc6d08..b082ee4 100644 --- a/users/views/admin/hashtags.py +++ b/users/views/admin/hashtags.py @@ -1,7 +1,8 @@ from django import forms from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator -from django.views.generic import FormView, ListView +from django.views.generic import FormView, ListView, View +from django_htmx.http import HttpResponseClientRefresh from activities.models import Hashtag, HashtagStates from users.decorators import moderator_required @@ -78,3 +79,18 @@ class HashtagEdit(FormView): "name_override": self.hashtag.name_override, "public": self.hashtag.public, } + + +@method_decorator(moderator_required, name="dispatch") +class HashtagEnable(View): + """ + Sets a hashtag to be enabled (or not!) + """ + + enable = True + + def post(self, request, hashtag): + self.hashtag = get_object_or_404(Hashtag, hashtag=hashtag) + self.hashtag.public = self.enable + self.hashtag.save() + return HttpResponseClientRefresh() diff --git a/users/views/admin/identities.py b/users/views/admin/identities.py index 4c45a57..50b9f7e 100644 --- a/users/views/admin/identities.py +++ b/users/views/admin/identities.py @@ -25,9 +25,11 @@ class IdentitiesRoot(ListView): return super().get(request, *args, **kwargs) def get_queryset(self): - identities = Identity.objects.annotate( - num_users=models.Count("users") - ).order_by("created") + identities = ( + Identity.objects.annotate(num_users=models.Count("users")) + .annotate(followers_count=models.Count("inbound_follows")) + .order_by("created") + ) if self.local_only: identities = identities.filter(local=True) if self.query: