mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-25 08:41:00 +00:00
Fixed #376: Emoji admin page
Also did a new table style for admin pages
This commit is contained in:
parent
d6f558f89a
commit
113db4ab3a
23 changed files with 674 additions and 229 deletions
|
@ -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:
|
||||
|
|
|
@ -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)(?!;)")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}">
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="{{ domain.urls.edit }}" class="overlay"></a>
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span class="handle">
|
||||
</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 %}
|
||||
|
|
67
templates/admin/emoji.html
Normal file
67
templates/admin/emoji.html
Normal 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 %}
|
18
templates/admin/emoji_create.html
Normal file
18
templates/admin/emoji_create.html
Normal 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 %}
|
|
@ -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 }}">
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="{{ domain.urls.edit_federation }}" class="overlay"></a>
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<span class="handle">
|
||||
</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>
|
||||
<small>{{ domain.software }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if domain.blocked %}
|
||||
<span class="pill bad">Blocked</span>
|
||||
<span class="bad">Blocked</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</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 %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
<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=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,40 +3,53 @@
|
|||
{% 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">
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
<small>{% if hashtag.public %}Public{% elif hashtag.public is None %}Unreviewed{% else %}Private{% endif %}</small>
|
||||
</td>
|
||||
<td class="stat">
|
||||
{% if hashtag.stats %}
|
||||
<div class="count">
|
||||
<span class="handle">
|
||||
{{ hashtag.stats.total }}
|
||||
<small>Total</small>
|
||||
</span>
|
||||
</div>
|
||||
<small>post{{ hashtag.stats.total|pluralize }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="stat">
|
||||
{% 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="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 %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
|
@ -48,5 +61,4 @@
|
|||
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 %}&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">
|
||||
<tr class="empty">
|
||||
<td>
|
||||
{% if query %}
|
||||
No identities match your query.
|
||||
{% else %}
|
||||
There are no identities yet.
|
||||
{% endif %}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if local_only %}&local_only=true{% endif %}{% if query %}&query={{ query }}{% endif %}">Previous Page</a>
|
||||
<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=".?page={{ page_obj.next_page_number }}{% if local_only %}&local_only=true{% endif %}{% if query %}&query={{ query }}{% endif %}">Next Page</a>
|
||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,41 +4,53 @@
|
|||
{% block subtitle %}Invites{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form>
|
||||
<div class="buttons">
|
||||
<div class="view-options">
|
||||
<span class="spacer"></span>
|
||||
<a href="{% url "admin_invite_create" %}" class="button">Create New</a>
|
||||
</div>
|
||||
</form>
|
||||
<section class="icon-menu">
|
||||
<table class="items">
|
||||
{% for invite in page_obj %}
|
||||
<a class="option" href="{{ invite.urls.admin_view }}">
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="{{ invite.urls.admin_view }}" class="overlay"></a>
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<span class="handle">
|
||||
</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
|
||||
</td>
|
||||
<td class="stat">
|
||||
{% if invite.expires %}
|
||||
{% if invite.valid %}
|
||||
{{ invite.expires|timeuntil }}
|
||||
<small>until expiry</small>
|
||||
{% else %}
|
||||
Infinite uses
|
||||
<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 %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
|
@ -50,5 +62,4 @@
|
|||
<a class="button" href=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,26 +11,39 @@
|
|||
<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 }}">
|
||||
<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 }}">
|
||||
<span class="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 }})
|
||||
{% endif %}
|
||||
<small>
|
||||
{{ report.type|title }}
|
||||
Post on {{ report.subject_post.published }}
|
||||
</small>
|
||||
</span>
|
||||
<time>{{ report.created|timedeltashort }} ago</time>
|
||||
</a>
|
||||
{% endif %}
|
||||
</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">
|
||||
<tr class="empty">
|
||||
<td>
|
||||
There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}{% if all %}&all=true{% endif %}">Previous Page</a>
|
||||
|
@ -42,5 +55,4 @@
|
|||
<a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&all=true{% endif %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 }}">
|
||||
<tr>
|
||||
<td class="icon">
|
||||
<a href="{{ user.urls.admin_edit }}" class="overlay"></a>
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span class="handle">
|
||||
</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>
|
||||
<td class="actions">
|
||||
{% if user.banned %}
|
||||
<span class="pill bad">Banned</span>
|
||||
<span class="bad">Banned</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<p class="option empty">
|
||||
<tr class="empty">
|
||||
<td>
|
||||
{% if query %}
|
||||
No users match your query.
|
||||
{% else %}
|
||||
There are no users yet.
|
||||
{% endif %}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="button" href=".?page={{ page_obj.previous_page_number }}">Previous Page</a>
|
||||
<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=".?page={{ page_obj.next_page_number }}">Next Page</a>
|
||||
<a class="button" href=".?{% urlparams page=page_obj.next_page_number %}">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
96
users/views/admin/emoji.py
Normal file
96
users/views/admin/emoji.py
Normal 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()
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue