summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activities/models/post.py4
-rw-r--r--core/ld.py1
-rw-r--r--takahe/urls.py7
-rw-r--r--templates/admin/user_edit.html36
-rw-r--r--templates/admin/users.html39
-rw-r--r--tests/users/models/test_identity.py2
-rw-r--r--users/admin.py8
-rw-r--r--users/models/identity.py2
-rw-r--r--users/models/inbox_message.py7
-rw-r--r--users/models/system_actor.py2
-rw-r--r--users/models/user.py5
-rw-r--r--users/views/admin/__init__.py15
-rw-r--r--users/views/admin/users.py84
13 files changed, 189 insertions, 23 deletions
diff --git a/activities/models/post.py b/activities/models/post.py
index b5821f7..e8698d3 100644
--- a/activities/models/post.py
+++ b/activities/models/post.py
@@ -659,7 +659,7 @@ class Post(StatorModel):
if update or created:
post.content = data["content"]
post.summary = data.get("summary")
- post.sensitive = data.get("as:sensitive", False)
+ post.sensitive = data.get("sensitive", False)
post.url = data.get("url")
post.published = parse_ld_date(data.get("published"))
post.edited = parse_ld_date(data.get("updated"))
@@ -670,7 +670,7 @@ class Post(StatorModel):
if tag["type"].lower() == "mention":
mention_identity = Identity.by_actor_uri(tag["href"], create=True)
post.mentions.add(mention_identity)
- elif tag["type"].lower() == "as:hashtag":
+ elif tag["type"].lower() == "hashtag":
post.hashtags.append(tag["name"].lower().lstrip("#"))
elif tag["type"].lower() == "http://joinmastodon.org/ns#emoji":
emoji = Emoji.by_ap_tag(post.author.domain, tag, create=True)
diff --git a/core/ld.py b/core/ld.py
index 950bf06..24088ec 100644
--- a/core/ld.py
+++ b/core/ld.py
@@ -415,6 +415,7 @@ def canonicalise(json_data: dict, include_security: bool = False) -> dict:
"votersCount": "toot:votersCount",
"Hashtag": "as:Hashtag",
"Public": "as:Public",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
},
]
if include_security:
diff --git a/takahe/urls.py b/takahe/urls.py
index 6bb239b..1e02622 100644
--- a/takahe/urls.py
+++ b/takahe/urls.py
@@ -96,10 +96,15 @@ urlpatterns = [
),
path(
"admin/users/",
- admin.Users.as_view(),
+ admin.UsersRoot.as_view(),
name="admin_users",
),
path(
+ "admin/users/<id>/",
+ admin.UserEdit.as_view(),
+ name="admin_user_edit",
+ ),
+ path(
"admin/identities/",
admin.Identities.as_view(),
name="admin_identities",
diff --git a/templates/admin/user_edit.html b/templates/admin/user_edit.html
new file mode 100644
index 0000000..b795848
--- /dev/null
+++ b/templates/admin/user_edit.html
@@ -0,0 +1,36 @@
+{% extends "settings/base.html" %}
+
+{% block subtitle %}{{ user.email }}{% endblock %}
+
+{% block content %}
+ <h1>{{ editing_user.email }}</h1>
+ <form action="." method="POST">
+ {% csrf_token %}
+ <fieldset>
+ <legend>Permissions</legend>
+ {% include "forms/_field.html" with field=form.status %}
+ {% if same_user %}
+ <ul class="errorlist">
+ <li>You cannot edit your own permission status!</li>
+ </ul>
+ {% endif %}
+ </fieldset>
+ <fieldset>
+ <legend>Identities</legend>
+ {% for identity in editing_user.identities.all %}
+ {% include "activities/_identity.html" %}
+ {% empty %}
+ <p>This user has no identities yet.</p>
+ {% endfor %}
+ </fieldset>
+ <fieldset>
+ <legend>Dates</legend>
+ <p>Last seen: <time title="{{ editing_user.last_seen }} UTC">{{ editing_user.last_seen | timesince }} ago</time></p>
+ <p>Created: <time title="{{ editing_user.created }} UTC">{{ editing_user.created | timesince }} ago</time></p>
+ </fieldset>
+ <div class="buttons">
+ <a href="{{ editing_user.urls.admin }}" class="button secondary left">Back</a>
+ <button>Save</button>
+ </div>
+ </form>
+{% endblock %}
diff --git a/templates/admin/users.html b/templates/admin/users.html
index f2dc864..1bf5b2e 100644
--- a/templates/admin/users.html
+++ b/templates/admin/users.html
@@ -3,7 +3,40 @@
{% block subtitle %}Users{% endblock %}
{% block content %}
- <p>
- Please use the <a href="/djadmin/users/user/">Django Admin</a> for now.
- </p>
+ <form action="." class="search">
+ <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">
+ {% for user in page_obj %}
+ <a class="option" href="{{ user.urls.admin_edit }}">
+ <i class="fa-solid fa-user"></i>
+ <span class="handle">
+ {{ user.email }}
+ <small>
+ {{ user.num_identities }} identit{{ user.num_identities|pluralize:"y,ies" }}
+ </small>
+ </span>
+ {% if user.banned %}
+ <span class="pill bad">Banned</span>
+ {% endif %}
+ </a>
+ {% empty %}
+ <p class="option empty">
+ {% if query %}
+ No users match your query.
+ {% else %}
+ There are no users yet.
+ {% endif %}
+ </p>
+ {% endfor %}
+ <div class="load-more">
+ {% 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 }}">Next Page</a>
+ {% endif %}
+ </div>
+ </section>
{% endblock %}
diff --git a/tests/users/models/test_identity.py b/tests/users/models/test_identity.py
index 13c08f0..182f2a9 100644
--- a/tests/users/models/test_identity.py
+++ b/tests/users/models/test_identity.py
@@ -155,7 +155,7 @@ def test_fetch_actor(httpx_mock, config_system):
"mediaType": "image/jpeg",
"url": "https://example.com/image.jpg",
},
- "as:manuallyApprovesFollowers": False,
+ "manuallyApprovesFollowers": False,
"name": "Test User",
"preferredUsername": "test",
"published": "2022-11-02T00:00:00Z",
diff --git a/users/admin.py b/users/admin.py
index 0b502d5..6c89881 100644
--- a/users/admin.py
+++ b/users/admin.py
@@ -89,7 +89,13 @@ class PasswordResetAdmin(admin.ModelAdmin):
@admin.register(InboxMessage)
class InboxMessageAdmin(admin.ModelAdmin):
- list_display = ["id", "state", "state_changed", "message_type", "message_actor"]
+ list_display = [
+ "id",
+ "state",
+ "state_changed",
+ "message_type_full",
+ "message_actor",
+ ]
list_filter = ("state",)
search_fields = ["message"]
actions = ["reset_state"]
diff --git a/users/models/identity.py b/users/models/identity.py
index 2e13de1..574e54e 100644
--- a/users/models/identity.py
+++ b/users/models/identity.py
@@ -438,7 +438,7 @@ class Identity(StatorModel):
self.username = self.username["@value"]
if self.username:
self.username = self.username.lower()
- self.manually_approves_followers = document.get("as:manuallyApprovesFollowers")
+ self.manually_approves_followers = document.get("manuallyApprovesFollowers")
self.public_key = document.get("publicKey", {}).get("publicKeyPem")
self.public_key_id = document.get("publicKey", {}).get("id")
self.icon_uri = document.get("icon", {}).get("url")
diff --git a/users/models/inbox_message.py b/users/models/inbox_message.py
index 0bf6851..526311d 100644
--- a/users/models/inbox_message.py
+++ b/users/models/inbox_message.py
@@ -116,5 +116,12 @@ class InboxMessage(StatorModel):
return self.message["object"]["type"].lower()
@property
+ def message_type_full(self):
+ if isinstance(self.message.get("object"), dict):
+ return f"{self.message_type}.{self.message_object_type}"
+ else:
+ return f"{self.message_type}"
+
+ @property
def message_actor(self):
return self.message.get("actor")
diff --git a/users/models/system_actor.py b/users/models/system_actor.py
index fb5a9e1..46a0007 100644
--- a/users/models/system_actor.py
+++ b/users/models/system_actor.py
@@ -48,7 +48,7 @@ class SystemActor:
},
"preferredUsername": self.username,
"url": self.profile_uri,
- "as:manuallyApprovesFollowers": True,
+ "manuallyApprovesFollowers": True,
"publicKey": {
"id": self.public_key_id,
"owner": self.actor_uri,
diff --git a/users/models/user.py b/users/models/user.py
index e0cac9d..8e3dc59 100644
--- a/users/models/user.py
+++ b/users/models/user.py
@@ -1,3 +1,4 @@
+import urlman
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
@@ -44,6 +45,10 @@ class User(AbstractBaseUser):
objects = UserManager()
+ class urls(urlman.Urls):
+ admin = "/admin/users/"
+ admin_edit = "{admin}{self.pk}/"
+
@property
def is_active(self):
return not (self.deleted or self.banned)
diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py
index bb70ff7..5ace04d 100644
--- a/users/views/admin/__init__.py
+++ b/users/views/admin/__init__.py
@@ -3,7 +3,7 @@ from django.utils.decorators import method_decorator
from django.views.generic import FormView, RedirectView, TemplateView
from users.decorators import admin_required
-from users.models import Identity, User
+from users.models import Identity
from users.views.admin.domains import ( # noqa
DomainCreate,
DomainDelete,
@@ -23,6 +23,7 @@ from users.views.admin.settings import ( # noqa
TuningSettings,
)
from users.views.admin.stator import Stator # noqa
+from users.views.admin.users import UserEdit, UsersRoot # noqa
@method_decorator(admin_required, name="dispatch")
@@ -31,18 +32,6 @@ class AdminRoot(RedirectView):
@method_decorator(admin_required, name="dispatch")
-class Users(TemplateView):
-
- template_name = "admin/users.html"
-
- def get_context_data(self):
- return {
- "users": User.objects.order_by("email"),
- "section": "users",
- }
-
-
-@method_decorator(admin_required, name="dispatch")
class Identities(TemplateView):
template_name = "admin/identities.html"
diff --git a/users/views/admin/users.py b/users/views/admin/users.py
new file mode 100644
index 0000000..fab4616
--- /dev/null
+++ b/users/views/admin/users.py
@@ -0,0 +1,84 @@
+from django import forms
+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
+
+from users.decorators import admin_required
+from users.models import User
+
+
+@method_decorator(admin_required, name="dispatch")
+class UsersRoot(ListView):
+
+ template_name = "admin/users.html"
+ paginate_by = 50
+
+ def get(self, request, *args, **kwargs):
+ self.query = request.GET.get("query")
+ self.extra_context = {
+ "section": "users",
+ "query": self.query or "",
+ }
+ return super().get(request, *args, **kwargs)
+
+ def get_queryset(self):
+ users = User.objects.annotate(
+ num_identities=models.Count("identities")
+ ).order_by("created")
+ if self.query:
+ users = users.filter(email__icontains=self.query)
+ return users
+
+
+@method_decorator(admin_required, name="dispatch")
+class UserEdit(FormView):
+
+ template_name = "admin/user_edit.html"
+ extra_context = {
+ "section": "users",
+ }
+
+ class form_class(forms.Form):
+ status = forms.ChoiceField(
+ choices=[
+ ("normal", "Normal User"),
+ ("moderator", "Moderator"),
+ ("admin", "Admin"),
+ ("banned", "Banned"),
+ ]
+ )
+
+ def dispatch(self, request, id, *args, **kwargs):
+ self.user = get_object_or_404(User, id=id)
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_initial(self):
+ status = "normal"
+ if self.user.moderator:
+ status = "moderator"
+ if self.user.admin:
+ status = "admin"
+ if self.user.banned:
+ status = "banned"
+ return {
+ "email": self.user.email,
+ "status": status,
+ }
+
+ def form_valid(self, form):
+ # Don't let them change themselves
+ if self.user == self.request.user:
+ return redirect(".")
+ status = form.cleaned_data["status"]
+ self.user.banned = status == "banned"
+ self.user.moderator = status == "moderator"
+ self.user.admin = status == "admin"
+ self.user.save()
+ return redirect(self.user.urls.admin)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["editing_user"] = self.user
+ context["same_user"] = self.user == self.request.user
+ return context