Fixed #376: Emoji admin page

Also did a new table style for admin pages
This commit is contained in:
Andrew Godwin 2023-01-10 20:31:50 -07:00
parent d6f558f89a
commit 113db4ab3a
23 changed files with 674 additions and 229 deletions

View File

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

View File

@ -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)(?!;)")

View File

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

View File

@ -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;
}

View File

@ -148,6 +148,23 @@ urlpatterns = [
"admin/hashtags/<hashtag>/",
admin.HashtagEdit.as_view(),
),
path("admin/hashtags/<hashtag>/enable/", admin.HashtagEnable.as_view()),
path(
"admin/hashtags/<hashtag>/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/<id>/enable/", admin.EmojiEnable.as_view()),
path("admin/emoji/<id>/disable/", admin.EmojiEnable.as_view(enable=False)),
path("admin/emoji/<id>/delete/", admin.EmojiDelete.as_view()),
path(
"admin/stator/",
admin.Stator.as_view(),

View File

@ -15,14 +15,8 @@
and you will <b>not be able to delete a domain with identities on it</b>.
</p>
<p>
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 <tt>/.well-known/webfinger</tt>, <tt>/.well-known/host-meta</tt>
and <tt>/.well-known/nodeinfo</tt> from the main domain to the
service domain.
For more information about domain setup, including what service
domains are, see <a href="https://docs.jointakahe.org/en/latest/domains/">our documentation on domains</a>.
</p>
{% csrf_token %}
<fieldset>

View File

@ -3,26 +3,31 @@
{% block subtitle %}Domains{% endblock %}
{% block content %}
<section class="icon-menu">
<div class="view-options">
<span class="spacer"></span>
<a href="{% url "admin_domains_create" %}" class="button"><i class="fa-solid fa-plus"></i> Add Domain</a>
</div>
<table class="items">
{% for domain in domains %}
<a class="option" href="{{ domain.urls.edit }}">
<i class="fa-solid fa-globe"></i>
<span class="handle">
<tr>
<td class="icon">
<a href="{{ domain.urls.edit }}" class="overlay"></a>
<i class="fa-solid fa-globe"></i>
</td>
<td class="name">
<a href="{{ domain.urls.edit }}" class="overlay"></a>
{{ domain.domain }}
<small>
{% if domain.public %}Public{% else %}Private{% endif %}
{% if domain.service_domain %}({{ domain.service_domain }}){% endif %}
{% if domain.service_domain %}{{ domain.service_domain }}{% endif %}
</small>
</span>
{% if domain.default %}
<span class="pill">Default</span>
{% endif %}
</a>
<td class="stat">
{% if domain.public %}Public{% else %}Private{% endif %}
{% if domain.default %}(Default){% endif %}
</td>
</tr>
{% empty %}
<p class="option empty">You have no domains set up.</p>
<tr class="empty"><td>You have no domains set up.</td></tr>
{% endfor %}
<a href="{% url "admin_domains_create" %}" class="option new">
<i class="fa-solid fa-plus"></i> Add a domain
</a>
</section>
</table>
{% endblock %}

View File

@ -0,0 +1,67 @@
{% extends "settings/base.html" %}
{% load activity_tags %}
{% block subtitle %}Emoji{% endblock %}
{% block content %}
<form action="." class="search">
<input type="search" name="query" value="{{ query }}" placeholder="Search by shortcode or domain">
{% if local_only %}
<input type="hidden" name="local_only" value="true">
{% endif %}
<button><i class="fa-solid fa-search"></i></button>
</form>
<div class="view-options">
{% if local_only %}
<a href=".?{% urlparams local_only=False %}" class="selected"><i class="fa-solid fa-check"></i> Local Only</a>
{% else %}
<a href=".?{% urlparams local_only=True %}"><i class="fa-solid fa-xmark"></i> Local Only</a>
{% endif %}
<a href="{% url "admin_emoji_create" %}" class="button">Add Emoji</a>
</div>
<table class="items">
{% for emoji in page_obj %}
<tr>
<td class="icon">
<img src="{{ emoji.full_url_admin.relative }}" class="icon">
</td>
<td class="name">
{{ emoji.shortcode }}
{% if emoji.domain %}<small>{{ emoji.domain }}</small>{% endif %}
</td>
<td>
</td>
<td class="actions">
{% if not emoji.is_usable %}
<span class="bad">Disabled</span>
<a hx-post="{{ emoji.urls.admin_enable }}" title="Enable"><i class="fa-solid fa-circle-check"></i></a>
{% else %}
<a hx-post="{{ emoji.urls.admin_disable }}" class="danger" title="Disable"><i class="fa-solid fa-circle-xmark"></i></a>
{% endif %}
<a hx-post="{{ emoji.urls.admin_delete }}" hx-confirm="Are you sure you want to delete :{{ emoji.shortcode }}:?" class="danger" title="Delete"><i class="fa-solid fa-trash"></i></a>
</td>
</tr>
{% empty %}
<tr class="empty">
<td>
{% if query %}
No emoji match your query.
{% else %}
There are no emoji yet.
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} emoji</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "settings/base.html" %}
{% block subtitle %}{{ emoji.shortcode }}{% endblock %}
{% block content %}
<form action="." method="POST" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
<legend>Emoji Details</legend>
{% include "forms/_field.html" with field=form.shortcode %}
{% include "forms/_field.html" with field=form.image hide_existing=True %}
</fieldset>
<div class="buttons">
<a href="{% url "admin_emoji" %}" class="button secondary left">Back</a>
<button>Save</button>
</div>
</form>
{% endblock %}

View File

@ -1,4 +1,5 @@
{% extends "settings/base.html" %}
{% load activity_tags %}
{% block subtitle %}Federation{% endblock %}
@ -7,33 +8,49 @@
<input type="search" name="query" value="{{ query }}" placeholder="Search by domain">
<button><i class="fa-solid fa-search"></i></button>
</form>
<section class="icon-menu">
<table class="items">
{% for domain in page_obj %}
<a class="option" href="{{ domain.urls.edit_federation }}">
<i class="fa-solid fa-globe"></i>
<span class="handle">
<tr>
<td class="icon">
<a href="{{ domain.urls.edit_federation }}" class="overlay"></a>
<i class="fa-solid fa-globe"></i>
</td>
<td class="name">
<a href="{{ domain.urls.edit_federation }}" class="overlay"></a>
{{ domain.domain }}
<small>
{{ domain.num_users }} remote identit{{ domain.num_users|pluralize:"y,ies" }}
</small>
</span>
{% if domain.blocked %}
<span class="pill bad">Blocked</span>
{% endif %}
</a>
<small>{{ domain.software }}</small>
</td>
<td>
{% if domain.blocked %}
<span class="bad">Blocked</span>
{% endif %}
</td>
<td class="stat">
{{ domain.num_users }}
<small>identit{{ domain.num_users|pluralize:"y,ies" }}</small>
</td>
</tr>
{% empty %}
<p class="option empty">There are no federation links yet.</p>
<tr class="empty">
<td>
{% if query %}
There are no domains matching your query.
{% else %}
There are no federation links yet.
{% endif %}
</td>
</tr>
{% 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.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} domain{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
</section>
</table>
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} domain{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -11,7 +11,7 @@
{% include "forms/_field.html" with field=form.blocked %}
</fieldset>
<div class="buttons">
<a href="{{ domain.urls.root }}" class="button secondary left">Back</a>
<a href="{{ domain.urls.root_federation }}" class="button secondary left">Back</a>
<a href="{{ domain.urls.delete }}" class="button delete">Delete</a>
<button>Save</button>
</div>

View File

@ -3,50 +3,62 @@
{% block subtitle %}Hashtags{% endblock %}
{% block content %}
<section class="icon-menu">
<table class="items">
{% for hashtag in page_obj %}
<a class="option hashtags" href="{{ hashtag.urls.admin_edit }}">
<div class="tag">
<tr>
<td class="icon">
<a href="{{ hashtag.urls.admin_edit }}" class="overlay"></a>
<i class="fa-solid fa-hashtag"></i>
<span class="handle">
{{ hashtag.display_name }}
<small>
{% if hashtag.public %}Public{% elif hashtag.public is None %}Unreviewed{% else %}Private{% endif %}
</small>
</span>
</div>
{% if hashtag.stats %}
<div class="count">
<span class="handle">
{{ hashtag.stats.total }}
<small>Total</small>
</span>
</div>
{% endif %}
{% if hashtag.aliases %}
<div class="count">
<span class="handle">
{% for alias in hashtag.aliases %}
{{ alias }}{% if not forloop.last %}, {% endif %}
{% endfor %}
<small>Aliases</small>
</span>
</div>
{% endif %}
</a>
</td>
<td class="name">
<a href="{{ hashtag.urls.admin_edit }}" class="overlay"></a>
{{ hashtag.display_name }}
<small>{% if hashtag.public %}Public{% elif hashtag.public is None %}Unreviewed{% else %}Private{% endif %}</small>
</td>
<td class="stat">
{% if hashtag.stats %}
{{ hashtag.stats.total }}
<small>post{{ hashtag.stats.total|pluralize }}</small>
{% endif %}
</td>
<td class="stat">
{% if hashtag.aliases %}
{% for alias in hashtag.aliases %}
{{ alias }}{% if not forloop.last %}, {% endif %}
{% endfor %}
<small>Aliases</small>
{% endif %}
</td>
<td class="actions">
{% if hashtag.public is not True %}
<a hx-post="{{ hashtag.urls.admin_enable }}" title="Make Public"><i class="fa-solid fa-circle-check"></i></a>
{% endif %}
{% if hashtag.public is not False %}
<a hx-post="{{ hashtag.urls.admin_disable }}" title="Make Private"><i class="fa-solid fa-circle-xmark"></i></a>
{% endif %}
</td>
</tr>
{% empty %}
<p class="option empty">There are no hashtags yet.</p>
<tr class="empty">
<td>
{% if query %}
No hashtags match your query.
{% else %}
There are no hashtags yet.
{% endif %}
</td>
</tr>
{% 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.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} hashtag{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
</section>
</table>
<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.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} hashtag{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -1,4 +1,5 @@
{% extends "settings/base.html" %}
{% load activity_tags %}
{% block subtitle %}Identities{% endblock %}
@ -12,42 +13,71 @@
</form>
<div class="view-options">
{% if local_only %}
<a href=".?{% if query %}query={{ query }}{% endif %}" class="selected"><i class="fa-solid fa-check"></i> Local Only</a>
<a href=".?{% urlparams local_only=False %}" class="selected"><i class="fa-solid fa-check"></i> Local Only</a>
{% else %}
<a href=".?local_only=true{% if query %}&amp;query={{ query }}{% endif %}"><i class="fa-solid fa-xmark"></i> Local Only</a>
<a href=".?{% urlparams local_only=True %}"><i class="fa-solid fa-xmark"></i> Local Only</a>
{% endif %}
</div>
<section class="icon-menu">
<table class="items">
{% for identity in page_obj %}
<a class="option" href="{{ identity.urls.admin_edit }}">
<div class="option-content">
{% include "identity/_identity_banner.html" with identity=identity link_avatar=False link_handle=False %}
</div>
<div class="option-actions">
{% if identity.banned %}
<span class="pill bad">Banned</span>
<tr>
<td class="icon">
<a href="{{ identity.urls.admin_edit }}" class="overlay"></a>
<img
src="{{ identity.local_icon_url.relative }}"
class="icon"
alt="Avatar for {{ identity.name_or_handle }}"
loading="lazy"
data-handle="{{ identity.name_or_handle }}"
_="on error set my.src to generate_avatar(@data-handle)"
>
</td>
<td class="name">
<a href="{{ identity.urls.admin_edit }}" class="overlay"></a>
{{ identity.html_name_or_handle }}
<small>@{{ identity.handle }}</small>
</td>
<td>
{% if identity.restriction == 1 %}
<span class="bad">Limited</span>
{% elif identity.restriction == 2 %}
<span class="bad">Blocked</span>
{% endif %}
</div>
</a>
</td>
<td class="stat">
{% if identity.local %}
Local
<small>{{ identity.followers_count }} follower{{ identity.followers_count|pluralize }}</small>
{% else %}
Remote
<small>{{ identity.followers_count }} local follower{{ identity.followers_count|pluralize }}</small>
{% endif %}
</td>
<td class="actions">
<a href="{{ identity.urls.admin_edit }}" title="View"><i class="fa-solid fa-eye"></i></a>
</td>
</tr>
{% empty %}
<p class="option empty">
{% if query %}
No identities match your query.
{% else %}
There are no identities yet.
{% endif %}
</p>
<tr class="empty">
<td>
{% if query %}
No identities match your query.
{% else %}
There are no identities yet.
{% endif %}
</td>
</tr>
{% endfor %}
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if local_only %}&amp;local_only=true{% endif %}{% if query %}&amp;query={{ query }}{% endif %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} identit{{page_obj.paginator.count|pluralize:"y,ies" }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if local_only %}&amp;local_only=true{% endif %}{% if query %}&amp;query={{ query }}{% endif %}">Next Page</a>
{% endif %}
</div>
</section>
</table>
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} identit{{page_obj.paginator.count|pluralize:"y,ies" }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -4,51 +4,62 @@
{% block subtitle %}Invites{% endblock %}
{% block content %}
<form>
<div class="buttons">
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
</div>
</form>
<section class="icon-menu">
<div class="view-options">
<span class="spacer"></span>
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
</div>
<table class="items">
{% for invite in page_obj %}
<a class="option" href="{{ invite.urls.admin_view }}">
<i class="fa-solid fa-envelope"></i>
<span class="handle">
<tr>
<td class="icon">
<a href="{{ invite.urls.admin_view }}" class="overlay"></a>
<i class="fa-solid fa-envelope"></i>
</td>
<td class="name">
<a href="{{ invite.urls.admin_view }}" class="overlay"></a>
{{ invite.token }}
<small>
{% if invite.expires %}
Expires in {{ invite.expires|timeuntil }}
{% if invite.note %}|{% endif %}
{% endif %}
{% if invite.note %}
{{ invite.note }}
{% endif %}
</small>
</span>
<time>
{% if invite.uses %}
{{ invite.uses }} use{{ invite.uses|pluralize }} left
{% else %}
Infinite uses
</td>
<td class="stat">
{% if invite.expires %}
{% if invite.valid %}
{{ invite.expires|timeuntil }}
<small>until expiry</small>
{% else %}
<span class="bad">Expired</span>
{% endif %}
{% endif %}
</time>
</a>
<td class="stat">
{% if invite.uses %}
{{ invite.uses }}
{% else %}
Infinite
{% endif %}
<small>use{{ invite.uses|pluralize }} left</small>
</time>
</tr>
{% empty %}
<p class="option empty">
There are no unused invites.
</p>
<tr class="empty">
<td>
There are no invites yet.
</td>
</tr>
{% 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.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} invite{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
</section>
</table>
<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.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} invite{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -11,36 +11,48 @@
<a href=".?all=true"><i class="fa-solid fa-xmark"></i> Show Resolved</a>
{% endif %}
</div>
<section class="icon-menu">
<table class="items">
{% for report in page_obj %}
<a class="option" href="{{ report.urls.admin_view }}">
<img src="{{ report.subject_identity.local_icon_url.relative }}" class="icon" alt="Avatar for {{ report.subject_identity.name_or_handle }}">
<span class="handle">
<tr>
<td class="icon">
<a href="{{ report.urls.admin_view }}" class="overlay"></a>
<img src="{{ report.subject_identity.local_icon_url.relative }}" class="icon" alt="Avatar for {{ report.subject_identity.name_or_handle }}">
</td>
<td class="name">
<a href="{{ report.urls.admin_view }}" class="overlay"></a>
{{ report.subject_identity.html_name_or_handle }}
{% if report.subject_post %}
(post {{ report.subject_post.pk }})
<small>
Post on {{ report.subject_post.published }}
</small>
{% endif %}
<small>
{{ report.type|title }}
</small>
</span>
<time>{{ report.created|timedeltashort }} ago</time>
</a>
</td>
<td class="stat">
{{ report.type|title }}
<small>Type</small>
</td>
<td class="stat">
{{ report.created|timedeltashort }}
<small>Reported</small>
</td>
</tr>
{% empty %}
<p class="option empty">
There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}.
</p>
<tr class="empty">
<td>
There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}.
</td>
</tr>
{% endfor %}
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if all %}&amp;all=true{% endif %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} report{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&amp;all=true{% endif %}">Next Page</a>
{% endif %}
</div>
</section>
</table>
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if all %}&amp;all=true{% endif %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} report{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&amp;all=true{% endif %}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -1,4 +1,5 @@
{% extends "settings/base.html" %}
{% load activity_tags %}
{% block subtitle %}Users{% endblock %}
@ -7,39 +8,51 @@
<input type="search" name="query" value="{{ query }}" placeholder="Search by email">
<button><i class="fa-solid fa-search"></i></button>
</form>
<section class="icon-menu">
<table class="items">
{% for user in page_obj %}
<a class="option" href="{{ user.urls.admin_edit }}">
<i class="fa-solid fa-user"></i>
<span class="handle">
<tr>
<td class="icon">
<a href="{{ user.urls.admin_edit }}" class="overlay"></a>
<i class="fa-solid fa-user"></i>
</td>
<td class="name">
<a href="{{ user.urls.admin_edit }}" class="overlay"></a>
{{ user.email }}
<small>{% if user.admin %}Admin{% elif user.moderator %}Moderator{% endif %}</small>
</td>
<td class="stat">
{{ user.num_identities }}
<small>
{{ user.num_identities }} identit{{ user.num_identities|pluralize:"y,ies" }}
identit{{ user.num_identities|pluralize:"y,ies" }}
</small>
</span>
{% if user.banned %}
<span class="pill bad">Banned</span>
{% endif %}
</a>
<td class="actions">
{% if user.banned %}
<span class="bad">Banned</span>
{% endif %}
</td>
</tr>
{% empty %}
<p class="option empty">
{% if query %}
No users match your query.
{% else %}
There are no users yet.
{% endif %}
</p>
<tr class="empty">
<td>
{% if query %}
No users match your query.
{% else %}
There are no users yet.
{% endif %}
</td>
</tr>
{% 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.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} user{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
{% endif %}
</div>
</section>
</table>
<div class="pagination">
{% if page_obj.has_previous %}
<a class="button" href=".?{% urlparams page=page_obj.previous_page_number %}">Previous Page</a>
{% endif %}
{% if page_obj.paginator.count %}
<span class="count">{{ page_obj.paginator.count }} user{{page_obj.paginator.count|pluralize }}</span>
{% endif %}
{% if page_obj.has_next %}
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
{% endif %}
</div>
{% endblock %}

View File

@ -10,14 +10,14 @@
</p>
{% 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 %}
<div class="clear">
<input type="checkbox" class="clear" name="{{ field.name }}__clear"> Clear current value</input>
</div>
{% endif %}
{{ field }}
</div>
{% if field.field.widget.input_type == "file" and field.value %}
{% if field.field.widget.input_type == "file" and field.value and not hide_existing %}
<img class="preview" src="{{ field.value }}">
{% endif %}
</div>

View File

@ -24,6 +24,9 @@
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
<i class="fa-solid fa-hashtag"></i> Hashtags
</a>
<a href="{% url "admin_emoji" %}" {% if section == "emoji" %}class="selected"{% endif %} title="Emoji">
<i class="fa-solid fa-icons"></i> Emoji
</a>
<a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
<i class="fa-solid fa-flag"></i> Reports
</a>

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

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