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,9 +96,14 @@ urlpatterns = [ ), path( "admin/users/", - admin.Users.as_view(), + admin.UsersRoot.as_view(), name="admin_users", ), + path( + "admin/users//", + admin.UserEdit.as_view(), + name="admin_user_edit", + ), path( "admin/identities/", admin.Identities.as_view(), 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 %} +

{{ editing_user.email }}

+
+ {% csrf_token %} +
+ Permissions + {% include "forms/_field.html" with field=form.status %} + {% if same_user %} +
    +
  • You cannot edit your own permission status!
  • +
+ {% endif %} +
+
+ Identities + {% for identity in editing_user.identities.all %} + {% include "activities/_identity.html" %} + {% empty %} +

This user has no identities yet.

+ {% endfor %} +
+
+ Dates +

Last seen:

+

Created:

+
+
+ Back + +
+
+{% 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 %} -

- Please use the Django Admin for now. -

+ +
+ {% for user in page_obj %} + + + + {{ user.email }} + + {{ user.num_identities }} identit{{ user.num_identities|pluralize:"y,ies" }} + + + {% if user.banned %} + Banned + {% endif %} + + {% empty %} +

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

+ {% endfor %} +
+ {% if page_obj.has_previous %} + Previous Page + {% endif %} + {% if page_obj.has_next %} + Next Page + {% endif %} +
+
{% 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 @@ -115,6 +115,13 @@ class InboxMessage(StatorModel): def message_object_type(self): 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") @@ -30,18 +31,6 @@ class AdminRoot(RedirectView): pattern_name = "admin_basic" -@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): 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