diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py
index 80b28310..1f1f1a3b 100644
--- a/bookwyrm/forms.py
+++ b/bookwyrm/forms.py
@@ -150,6 +150,12 @@ class LimitedEditUserForm(CustomForm):
help_texts = {f: None for f in fields}
+class UserGroupForm(CustomForm):
+ class Meta:
+ model = models.User
+ fields = ["groups"]
+
+
class TagForm(CustomForm):
class Meta:
model = models.Tag
diff --git a/bookwyrm/templates/book/publisher_info.html b/bookwyrm/templates/book/publisher_info.html
index a16332c5..b7975a62 100644
--- a/bookwyrm/templates/book/publisher_info.html
+++ b/bookwyrm/templates/book/publisher_info.html
@@ -1,6 +1,7 @@
{% spaceless %}
{% load i18n %}
+{% load humanize %}
{% with format=book.physical_format pages=book.pages %}
@@ -39,7 +40,7 @@
{% endif %}
- {% with date=book.published_date|date:'M jS Y' publisher=book.publishers|join:', ' %}
+ {% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date %}
-
-
-
{% trans "User details" %}
-
- {% include 'user/user_preview.html' with user=report.user %}
- {% if report.user.summary %}
-
- {{ report.user.summary | to_markdown | safe }}
-
- {% endif %}
+{% include 'user_admin/user_info.html' with user=report.user %}
-
{% trans "View user profile" %}
-
-
- {% if not report.user.local %}
- {% with server=report.user.federated_server %}
-
-
{% trans "Instance details" %}
-
- {% if server %}
-
{{ server.server_name }}
-
-
-
- {% trans "Software:" %}
- - {{ server.application_type }}
-
-
-
- {% trans "Version:" %}
- - {{ server.application_version }}
-
-
-
- {% trans "Status:" %}
- - {{ server.status }}
-
-
- {% if server.notes %}
-
{% trans "Notes" %}
-
- {{ server.notes }}
-
- {% endif %}
-
-
- {% trans "View instance" %}
-
- {% else %}
-
{% trans "Not set" %}
- {% endif %}
-
-
- {% endwith %}
- {% endif %}
-
-
-
-
{% trans "Actions" %}
-
-
+{% include 'user_admin/user_moderation_actions.html' with user=report.user %}
{% trans "Moderator Comments" %}
diff --git a/bookwyrm/templates/moderation/reports.html b/bookwyrm/templates/moderation/reports.html
index 72cadae5..f9d9d99b 100644
--- a/bookwyrm/templates/moderation/reports.html
+++ b/bookwyrm/templates/moderation/reports.html
@@ -30,7 +30,7 @@
-{% include 'settings/user_admin_filters.html' %}
+{% include 'user_admin/user_admin_filters.html' %}
{% if not reports %}
diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html
index 7c694d78..ba0a25cd 100644
--- a/bookwyrm/templates/notifications.html
+++ b/bookwyrm/templates/notifications.html
@@ -123,7 +123,7 @@
{% include 'snippets/status_preview.html' with status=related_status %}
- {{ related_status.published_date | post_date }}
+ {{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %}
diff --git a/bookwyrm/templates/snippets/status/status_body.html b/bookwyrm/templates/snippets/status/status_body.html
index 58d3fbf4..18f7876e 100644
--- a/bookwyrm/templates/snippets/status/status_body.html
+++ b/bookwyrm/templates/snippets/status/status_body.html
@@ -10,4 +10,4 @@
{% endif %}
{% endwith %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/bookwyrm/templates/settings/server_filter.html b/bookwyrm/templates/user_admin/server_filter.html
similarity index 100%
rename from bookwyrm/templates/settings/server_filter.html
rename to bookwyrm/templates/user_admin/server_filter.html
diff --git a/bookwyrm/templates/user_admin/user.html b/bookwyrm/templates/user_admin/user.html
new file mode 100644
index 00000000..46390650
--- /dev/null
+++ b/bookwyrm/templates/user_admin/user.html
@@ -0,0 +1,19 @@
+{% extends 'settings/admin_layout.html' %}
+{% load i18n %}
+{% load bookwyrm_tags %}
+{% load humanize %}
+
+{% block title %}{{ user.username }}{% endblock %}
+{% block header %}{{ user.username }}{% endblock %}
+
+{% block panel %}
+
+
+{% include 'user_admin/user_info.html' with user=user %}
+
+{% include 'user_admin/user_moderation_actions.html' with user=user %}
+
+{% endblock %}
+
diff --git a/bookwyrm/templates/settings/user_admin.html b/bookwyrm/templates/user_admin/user_admin.html
similarity index 93%
rename from bookwyrm/templates/settings/user_admin.html
rename to bookwyrm/templates/user_admin/user_admin.html
index a96d37f5..2ab526a9 100644
--- a/bookwyrm/templates/settings/user_admin.html
+++ b/bookwyrm/templates/user_admin/user_admin.html
@@ -13,7 +13,7 @@
{% block panel %}
-{% include 'settings/user_admin_filters.html' %}
+{% include 'user_admin/user_admin_filters.html' %}
@@ -41,7 +41,7 @@
{% for user in users %}
- {{ user.username }} |
+ {{ user.username }} |
{{ user.created_date }} |
{{ user.last_active_date }} |
{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %} |
diff --git a/bookwyrm/templates/settings/user_admin_filters.html b/bookwyrm/templates/user_admin/user_admin_filters.html
similarity index 51%
rename from bookwyrm/templates/settings/user_admin_filters.html
rename to bookwyrm/templates/user_admin/user_admin_filters.html
index a7b5c8aa..57e017e5 100644
--- a/bookwyrm/templates/settings/user_admin_filters.html
+++ b/bookwyrm/templates/user_admin/user_admin_filters.html
@@ -1,6 +1,6 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
-{% include 'settings/server_filter.html' %}
-{% include 'settings/username_filter.html' %}
+{% include 'user_admin/server_filter.html' %}
+{% include 'user_admin/username_filter.html' %}
{% endblock %}
diff --git a/bookwyrm/templates/user_admin/user_info.html b/bookwyrm/templates/user_admin/user_info.html
new file mode 100644
index 00000000..e5f5d580
--- /dev/null
+++ b/bookwyrm/templates/user_admin/user_info.html
@@ -0,0 +1,56 @@
+{% load i18n %}
+{% load bookwyrm_tags %}
+
+
+
{% trans "User details" %}
+
+ {% include 'user/user_preview.html' with user=user %}
+ {% if user.summary %}
+
+ {{ user.summary | to_markdown | safe }}
+
+ {% endif %}
+
+
{% trans "View user profile" %}
+
+
+ {% if not user.local %}
+ {% with server=user.federated_server %}
+
+
{% trans "Instance details" %}
+
+ {% if server %}
+
{{ server.server_name }}
+
+
+
- {% trans "Software:" %}
+ - {{ server.application_type }}
+
+
+
- {% trans "Version:" %}
+ - {{ server.application_version }}
+
+
+
- {% trans "Status:" %}
+ - {{ server.status }}
+
+
+ {% if server.notes %}
+
{% trans "Notes" %}
+
+ {{ server.notes }}
+
+ {% endif %}
+
+
+ {% trans "View instance" %}
+
+ {% else %}
+
{% trans "Not set" %}
+ {% endif %}
+
+
+ {% endwith %}
+ {% endif %}
+
+
diff --git a/bookwyrm/templates/user_admin/user_moderation_actions.html b/bookwyrm/templates/user_admin/user_moderation_actions.html
new file mode 100644
index 00000000..816e787a
--- /dev/null
+++ b/bookwyrm/templates/user_admin/user_moderation_actions.html
@@ -0,0 +1,42 @@
+{% load i18n %}
+
+
{% trans "Actions" %}
+
+ {% if user.local %}
+
+ {% endif %}
+
diff --git a/bookwyrm/templates/settings/username_filter.html b/bookwyrm/templates/user_admin/username_filter.html
similarity index 100%
rename from bookwyrm/templates/settings/username_filter.html
rename to bookwyrm/templates/user_admin/username_filter.html
diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py
index 52054ada..5d981909 100644
--- a/bookwyrm/templatetags/bookwyrm_tags.py
+++ b/bookwyrm/templatetags/bookwyrm_tags.py
@@ -129,28 +129,6 @@ def get_uuid(identifier):
return "%s%s" % (identifier, uuid4())
-@register.filter(name="post_date")
-def time_since(date):
- """ concise time ago function """
- if not isinstance(date, datetime):
- return ""
- now = timezone.now()
-
- if date < (now - relativedelta(weeks=1)):
- formatter = "%b %-d"
- if date.year != now.year:
- formatter += " %Y"
- return date.strftime(formatter)
- delta = relativedelta(now, date)
- if delta.days:
- return "%dd" % delta.days
- if delta.hours:
- return "%dh" % delta.hours
- if delta.minutes:
- return "%dm" % delta.minutes
- return "%ds" % delta.seconds
-
-
@register.filter(name="to_markdown")
def get_markdown(content):
""" convert markdown to html """
diff --git a/bookwyrm/tests/test_templatetags.py b/bookwyrm/tests/test_templatetags.py
index b4dc517f..2fadb978 100644
--- a/bookwyrm/tests/test_templatetags.py
+++ b/bookwyrm/tests/test_templatetags.py
@@ -181,36 +181,6 @@ class TemplateTags(TestCase):
uuid = bookwyrm_tags.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
- def test_time_since(self, _):
- """ ultraconcise timestamps """
- self.assertEqual(bookwyrm_tags.time_since("bleh"), "")
-
- now = timezone.now()
- self.assertEqual(bookwyrm_tags.time_since(now), "0s")
-
- seconds_ago = now - relativedelta(seconds=4)
- self.assertEqual(bookwyrm_tags.time_since(seconds_ago), "4s")
-
- minutes_ago = now - relativedelta(minutes=8)
- self.assertEqual(bookwyrm_tags.time_since(minutes_ago), "8m")
-
- hours_ago = now - relativedelta(hours=9)
- self.assertEqual(bookwyrm_tags.time_since(hours_ago), "9h")
-
- days_ago = now - relativedelta(days=3)
- self.assertEqual(bookwyrm_tags.time_since(days_ago), "3d")
-
- # I am not going to figure out how to mock dates tonight.
- months_ago = now - relativedelta(months=5)
- self.assertTrue(
- re.match(r"[A-Z][a-z]{2} \d?\d", bookwyrm_tags.time_since(months_ago))
- )
-
- years_ago = now - relativedelta(years=10)
- self.assertTrue(
- re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago))
- )
-
def test_get_markdown(self, _):
""" mardown format data """
result = bookwyrm_tags.get_markdown("_hi_")
diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py
index 1c56067a..bce19993 100644
--- a/bookwyrm/tests/views/test_reports.py
+++ b/bookwyrm/tests/views/test_reports.py
@@ -1,5 +1,4 @@
""" test for app action functionality """
-from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@@ -115,22 +114,19 @@ class ReportViews(TestCase):
report.refresh_from_db()
self.assertFalse(report.resolved)
- def test_deactivate_user(self):
+ def test_suspend_user(self):
""" toggle whether a user is able to log in """
self.assertTrue(self.rat.is_active)
- report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
# de-activate
- with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
- views.deactivate_user(request, report.id)
+ views.suspend_user(request, self.rat.id)
self.rat.refresh_from_db()
self.assertFalse(self.rat.is_active)
# re-activate
- with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
- views.deactivate_user(request, report.id)
+ views.suspend_user(request, self.rat.id)
self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active)
diff --git a/bookwyrm/tests/views/test_user_admin.py b/bookwyrm/tests/views/test_user_admin.py
index dd20c1b6..b1e9d639 100644
--- a/bookwyrm/tests/views/test_user_admin.py
+++ b/bookwyrm/tests/views/test_user_admin.py
@@ -1,4 +1,6 @@
""" test for app action functionality """
+from unittest.mock import patch
+from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@@ -21,9 +23,9 @@ class UserAdminViews(TestCase):
)
models.SiteSettings.objects.create()
- def test_user_admin_page(self):
+ def test_user_admin_list_page(self):
""" there are so many views, this just makes sure it LOADS """
- view = views.UserAdmin.as_view()
+ view = views.UserAdminList.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
@@ -31,3 +33,38 @@ class UserAdminViews(TestCase):
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
+
+ def test_user_admin_page(self):
+ """ there are so many views, this just makes sure it LOADS """
+ view = views.UserAdmin.as_view()
+ request = self.factory.get("")
+ request.user = self.local_user
+ request.user.is_superuser = True
+
+ result = view(request, self.local_user.id)
+
+ self.assertIsInstance(result, TemplateResponse)
+ result.render()
+ self.assertEqual(result.status_code, 200)
+
+ def test_user_admin_page_post(self):
+ """ set the user's group """
+ group = Group.objects.create(name="editor")
+ self.assertEqual(
+ list(self.local_user.groups.values_list("name", flat=True)), []
+ )
+
+ view = views.UserAdmin.as_view()
+ request = self.factory.post("", {"groups": [group.id]})
+ request.user = self.local_user
+ request.user.is_superuser = True
+
+ with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
+ result = view(request, self.local_user.id)
+
+ self.assertIsInstance(result, TemplateResponse)
+ result.render()
+
+ self.assertEqual(
+ list(self.local_user.groups.values_list("name", flat=True)), ["editor"]
+ )
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index 9e280f3e..8c5266a6 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -51,13 +51,20 @@ urlpatterns = [
r"^password-reset/(?P[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
),
# admin
- re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"),
+ re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
re_path(
- r"^settings/email-preview",
+ r"^settings/email-preview/?$",
views.site.email_preview,
name="settings-email-preview",
),
- re_path(r"^settings/users", views.UserAdmin.as_view(), name="settings-users"),
+ re_path(
+ r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users"
+ ),
+ re_path(
+ r"^settings/users/(?P\d+)/?$",
+ views.UserAdmin.as_view(),
+ name="settings-user",
+ ),
re_path(
r"^settings/federation/?$",
views.Federation.as_view(),
@@ -113,9 +120,9 @@ urlpatterns = [
name="settings-report",
),
re_path(
- r"^settings/reports/(?P\d+)/deactivate/?$",
- views.deactivate_user,
- name="settings-report-deactivate",
+ r"^settings/reports/(?P\d+)/suspend/?$",
+ views.suspend_user,
+ name="settings-report-suspend",
),
re_path(
r"^settings/reports/(?P\d+)/resolve/?$",
diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py
index 9f8463b4..c0f35ba8 100644
--- a/bookwyrm/views/__init__.py
+++ b/bookwyrm/views/__init__.py
@@ -25,7 +25,7 @@ from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate
-from .reports import Report, Reports, make_report, resolve_report, deactivate_user
+from .reports import Report, Reports, make_report, resolve_report, suspend_user
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search
@@ -37,5 +37,5 @@ from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag
from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following
-from .user_admin import UserAdmin
+from .user_admin import UserAdmin, UserAdminList
from .wellknown import *
diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py
index 3dd53cb9..07eb9b97 100644
--- a/bookwyrm/views/reports.py
+++ b/bookwyrm/views/reports.py
@@ -74,12 +74,13 @@ class Report(View):
@login_required
@permission_required("bookwyrm_moderate_user")
-def deactivate_user(_, report_id):
+def suspend_user(_, user_id):
""" mark an account as inactive """
- report = get_object_or_404(models.Report, id=report_id)
- report.user.is_active = not report.user.is_active
- report.user.save()
- return redirect("settings-report", report.id)
+ user = get_object_or_404(models.User, id=user_id)
+ user.is_active = not user.is_active
+ # this isn't a full deletion, so we don't want to tell the world
+ user.save(broadcast=False)
+ return redirect("settings-user", user.id)
@login_required
diff --git a/bookwyrm/views/user_admin.py b/bookwyrm/views/user_admin.py
index c0d097d7..4537abce 100644
--- a/bookwyrm/views/user_admin.py
+++ b/bookwyrm/views/user_admin.py
@@ -1,11 +1,12 @@
""" manage user """
from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
+from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
-from bookwyrm import models
+from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH
@@ -15,7 +16,7 @@ from bookwyrm.settings import PAGE_LENGTH
permission_required("bookwyrm.moderate_users", raise_exception=True),
name="dispatch",
)
-class UserAdmin(View):
+class UserAdminList(View):
""" admin view of users on this server """
def get(self, request):
@@ -49,4 +50,28 @@ class UserAdmin(View):
"sort": sort,
"server": server,
}
- return TemplateResponse(request, "settings/user_admin.html", data)
+ return TemplateResponse(request, "user_admin/user_admin.html", data)
+
+
+@method_decorator(login_required, name="dispatch")
+@method_decorator(
+ permission_required("bookwyrm.moderate_users", raise_exception=True),
+ name="dispatch",
+)
+class UserAdmin(View):
+ """ moderate an individual user """
+
+ def get(self, request, user):
+ """ user view """
+ user = get_object_or_404(models.User, id=user)
+ data = {"user": user, "group_form": forms.UserGroupForm()}
+ return TemplateResponse(request, "user_admin/user.html", data)
+
+ def post(self, request, user):
+ """ update user group """
+ user = get_object_or_404(models.User, id=user)
+ form = forms.UserGroupForm(request.POST, instance=user)
+ if form.is_valid():
+ form.save()
+ data = {"user": user, "group_form": form}
+ return TemplateResponse(request, "user_admin/user.html", data)