diff options
-rw-r--r-- | activities/models/post.py | 4 | ||||
-rw-r--r-- | core/ld.py | 1 | ||||
-rw-r--r-- | takahe/urls.py | 7 | ||||
-rw-r--r-- | templates/admin/user_edit.html | 36 | ||||
-rw-r--r-- | templates/admin/users.html | 39 | ||||
-rw-r--r-- | tests/users/models/test_identity.py | 2 | ||||
-rw-r--r-- | users/admin.py | 8 | ||||
-rw-r--r-- | users/models/identity.py | 2 | ||||
-rw-r--r-- | users/models/inbox_message.py | 7 | ||||
-rw-r--r-- | users/models/system_actor.py | 2 | ||||
-rw-r--r-- | users/models/user.py | 5 | ||||
-rw-r--r-- | users/views/admin/__init__.py | 15 | ||||
-rw-r--r-- | users/views/admin/users.py | 84 |
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) @@ -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 |