Merge branch 'main' into smaller-statuses-dense-cards

This commit is contained in:
Joachim 2021-04-21 17:40:20 +02:00 committed by GitHub
commit 2cacf5146b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 392 additions and 177 deletions

View file

@ -150,6 +150,12 @@ class LimitedEditUserForm(CustomForm):
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class TagForm(CustomForm): class TagForm(CustomForm):
class Meta: class Meta:
model = models.Tag model = models.Tag

View file

@ -109,7 +109,10 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p class="mb-2"><label class="label" for="id_series">{% trans "Series:" %}</label> {{ form.series }} </p> <p class="mb-2">
<label class="label" for="id_series">{% trans "Series:" %}</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
</p>
{% for error in form.series.errors %} {% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}

View file

@ -1,6 +1,7 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% load humanize %}
<p> <p>
{% with format=book.physical_format pages=book.pages %} {% with format=book.physical_format pages=book.pages %}
@ -39,7 +40,7 @@
{% endif %} {% endif %}
<p> <p>
{% 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 %} {% if date or book.first_published_date %}
<meta <meta
itemprop="datePublished" itemprop="datePublished"

View file

@ -30,26 +30,32 @@
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %} {% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
</div> </div>
</div> </div>
<div class="card-footer has-background-white-bis"> <div class="card-footer has-background-white-bis is-align-items-baseline">
<div class="card-footer-item"> <div class="card-footer-item">
<div>
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p> <p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
</div>
</div> </div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %} {% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<div class="card-footer-item">
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
<div class="field has-addons mb-0">
{% csrf_token %}
<div class="control">
<input id="input-list-position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
</div>
<div class="control">
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
</div>
</div>
<label for="input-list-position" class="help">{% trans "List position" %}</label>
</form>
</div>
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item"> <form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}"> <input type="hidden" name="item" value="{{ item.id }}">
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button> <button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
</form> </form>
</div>
<div class="card-footer has-background-white-bis">
<div>
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}" class="card-footer-item">
{% csrf_token %}
<label for="input-list-position" class="is-sr-only">{% trans "List position" %}</label>
<input id="input-list-position" class="input" type="number" min="1" name="position" value="{{ item.order }}">
<button type="submit" class="button is-small is-info">{% trans "List position" %}</button>
</form>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

@ -15,76 +15,9 @@
{% include 'moderation/report_preview.html' with report=report %} {% include 'moderation/report_preview.html' with report=report %}
</div> </div>
<div class="block columns"> {% include 'user_admin/user_info.html' with user=report.user %}
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=report.user %}
{% if report.user.summary %}
<div class="box content has-background-white-ter is-shadowless">
{{ report.user.summary | to_markdown | safe }}
</div>
{% endif %}
<p class="mt-2"><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p> {% include 'user_admin/user_moderation_actions.html' with user=report.user %}
</div>
</div>
{% if not report.user.local %}
{% with server=report.user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "Instance details" %}</h4>
<div class="box content is-flex-grow-1">
{% if server %}
<h5>{{ server.server_name }}</h5>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
{% if server.notes %}
<h5>{% trans "Notes" %}</h5>
<div class="box content has-background-white-ter is-shadowless">
{{ server.notes }}
</div>
{% endif %}
<p>
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
</p>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</div>
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' report.user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="deactivate" method="post" action="{% url 'settings-report-deactivate' report.id %}">
{% csrf_token %}
{% if report.user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Deactivate user" %}</button>
{% else %}
<button class="button">{% trans "Reactivate user" %}</button>
{% endif %}
</form>
</div>
</div>
<div class="block"> <div class="block">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3> <h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View file

@ -30,7 +30,7 @@
</ul> </ul>
</div> </div>
{% include 'settings/user_admin_filters.html' %} {% include 'user_admin/user_admin_filters.html' %}
<div class="block"> <div class="block">
{% if not reports %} {% if not reports %}

View file

@ -123,7 +123,7 @@
{% include 'snippets/status_preview.html' with status=related_status %} {% include 'snippets/status_preview.html' with status=related_status %}
</div> </div>
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}"> <div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
{{ related_status.published_date | post_date }} {{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %} {% include 'snippets/privacy-icons.html' with item=related_status %}
</div> </div>
</div> </div>

View file

@ -1,5 +1,3 @@
{% spaceless %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load i18n %} {% load i18n %}
@ -127,4 +125,4 @@
</div> </div>
{% endwith %} {% endwith %}
{% endspaceless %}

View file

@ -1,6 +1,5 @@
{% extends 'components/card.html' %} {% extends 'components/card.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load humanize %} {% load humanize %}

View file

@ -11,3 +11,4 @@
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,134 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% with status_type=status.status_type %}
<div
class="block"
{% if status_type == 'Review' %}
{% firstof "reviewBody" as body_prop %}
{% firstof 'itemprop="reviewRating" itemscope itemtype="https://schema.org/Rating"' as rating_type %}
{% endif %}
{% if status_type == 'Rating' %}
itemprop="rating"
itemtype="https://schema.org/Rating"
{% endif %}
>
{% if status_type == 'Review' or status_type == 'Rating' %}
<div>
{% if status.name %}
<h3
class="title is-5 has-subtitle"
dir="auto"
itemprop="name"
>
{{ status.name|escape }}
</h3>
{% endif %}
<span
class="is-sr-only"
{{ rating_type }}
>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{% if status_type == 'Rating' %}
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
{% endif %}
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
</div>
{% endif %}
{% if status.content_warning %}
<div>
<p>{{ status.content_warning }}</p>
{% trans "Show more" as button_text %}
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
{% include 'snippets/toggle/open_button.html' %}
{% endwith %}
</div>
{% endif %}
<div
{% if status.content_warning %}
id="show-status-cw-{{ status.id }}"
class="is-hidden"
{% endif %}
>
{% if status.content_warning %}
{% trans "Show less" as button_text %}
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
{% include 'snippets/toggle/close_button.html' %}
{% endwith %}
{% endif %}
{% if status.quote %}
<div class="quote block">
<blockquote dir="auto" class="content mb-2">{{ status.quote | safe }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
</div>
{% endif %}
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
{% with full=status.content|safe no_trim=status.content_warning itemprop=body_prop %}
{% include 'snippets/trimmed_text.html' %}
{% endwith %}
{% endif %}
{% if status.attachments.exists %}
<div class="block">
<div class="columns">
{% for attachment in status.attachments.all %}
<div class="column is-narrow">
<figure class="image is-128x128">
<a
href="/images/{{ attachment.image }}"
target="_blank"
aria-label="{% trans 'Open image in new window' %}"
>
<img
src="/images/{{ attachment.image }}"
{% if attachment.caption %}
alt="{{ attachment.caption }}"
title="{{ attachment.caption }}"
{% endif %}
>
</a>
</figure>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% if not hide_book %}
{% if status.book or status.mention_books.count %}
<div
{% if status_type != 'GeneratedNote' %}
class="box has-background-white-bis"
{% endif %}
>
{% if status.book %}
{% with book=status.book %}
{% include 'snippets/status/book_preview.html' %}
{% endwith %}
{% elif status.mention_books.count %}
{% with book=status.mention_books.first %}
{% include 'snippets/status/book_preview.html' %}
{% endwith %}
{% endif %}
</div>
{% endif %}
{% endif %}
{% endwith %}

View file

@ -1,4 +1,3 @@
{% spaceless %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load i18n %} {% load i18n %}
@ -46,4 +45,3 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endspaceless %}

View file

@ -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 %}
<div class="block">
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
</div>
{% include 'user_admin/user_info.html' with user=user %}
{% include 'user_admin/user_moderation_actions.html' with user=user %}
{% endblock %}

View file

@ -13,7 +13,7 @@
{% block panel %} {% block panel %}
{% include 'settings/user_admin_filters.html' %} {% include 'user_admin/user_admin_filters.html' %}
<table class="table is-striped"> <table class="table is-striped">
<tr> <tr>
@ -41,7 +41,7 @@
</tr> </tr>
{% for user in users %} {% for user in users %}
<tr> <tr>
<td>{{ user.username }}</td> <td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td>
<td>{{ user.created_date }}</td> <td>{{ user.created_date }}</td>
<td>{{ user.last_active_date }}</td> <td>{{ user.last_active_date }}</td>
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td> <td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>

View file

@ -1,6 +1,6 @@
{% extends 'snippets/filters_panel/filters_panel.html' %} {% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %} {% block filter_fields %}
{% include 'settings/server_filter.html' %} {% include 'user_admin/server_filter.html' %}
{% include 'settings/username_filter.html' %} {% include 'user_admin/username_filter.html' %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,56 @@
{% load i18n %}
{% load bookwyrm_tags %}
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=user %}
{% if user.summary %}
<div class="box content has-background-white-ter is-shadowless">
{{ user.summary | to_markdown | safe }}
</div>
{% endif %}
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
</div>
</div>
{% if not user.local %}
{% with server=user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "Instance details" %}</h4>
<div class="box content is-flex-grow-1">
{% if server %}
<h5>{{ server.server_name }}</h5>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
{% if server.notes %}
<h5>{% trans "Notes" %}</h5>
<div class="box content has-background-white-ter is-shadowless">
{{ server.notes }}
</div>
{% endif %}
<p>
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
</p>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</div>

View file

@ -0,0 +1,42 @@
{% load i18n %}
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}">
{% csrf_token %}
{% if user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
{% else %}
<button class="button">{% trans "Un-suspend user" %}</button>
{% endif %}
</form>
</div>
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>{{ name|title }}</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>User</option>
</select>
</div>
{% for error in group_form.groups.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
{% endwith %}
<button class="button">{% trans "Save" %}</button>
</form>
</div>
{% endif %}
</div>

View file

@ -1,11 +1,8 @@
""" template filters """ """ template filters """
from uuid import uuid4 from uuid import uuid4
from datetime import datetime
from dateutil.relativedelta import relativedelta
from django import template from django import template
from django.db.models import Avg from django.db.models import Avg
from django.utils import timezone
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.views.status import to_markdown from bookwyrm.views.status import to_markdown
@ -129,28 +126,6 @@ def get_uuid(identifier):
return "%s%s" % (identifier, uuid4()) 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") @register.filter(name="to_markdown")
def get_markdown(content): def get_markdown(content):
""" convert markdown to html """ """ convert markdown to html """

View file

@ -181,36 +181,6 @@ class TemplateTags(TestCase):
uuid = bookwyrm_tags.get_uuid("hi") uuid = bookwyrm_tags.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid)) 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, _): def test_get_markdown(self, _):
""" mardown format data """ """ mardown format data """
result = bookwyrm_tags.get_markdown("_hi_") result = bookwyrm_tags.get_markdown("_hi_")

View file

@ -1,5 +1,4 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -115,22 +114,19 @@ class ReportViews(TestCase):
report.refresh_from_db() report.refresh_from_db()
self.assertFalse(report.resolved) self.assertFalse(report.resolved)
def test_deactivate_user(self): def test_suspend_user(self):
""" toggle whether a user is able to log in """ """ toggle whether a user is able to log in """
self.assertTrue(self.rat.is_active) self.assertTrue(self.rat.is_active)
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
request = self.factory.post("") request = self.factory.post("")
request.user = self.local_user request.user = self.local_user
request.user.is_superuser = True request.user.is_superuser = True
# de-activate # de-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.suspend_user(request, self.rat.id)
views.deactivate_user(request, report.id)
self.rat.refresh_from_db() self.rat.refresh_from_db()
self.assertFalse(self.rat.is_active) self.assertFalse(self.rat.is_active)
# re-activate # re-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.suspend_user(request, self.rat.id)
views.deactivate_user(request, report.id)
self.rat.refresh_from_db() self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active) self.assertTrue(self.rat.is_active)

View file

@ -30,6 +30,14 @@ class UserViews(TestCase):
self.rat = models.User.objects.create_user( self.rat = models.User.objects.create_user(
"rat@local.com", "rat@rat.rat", "password", local=True, localname="rat" "rat@local.com", "rat@rat.rat", "password", local=True, localname="rat"
) )
self.book = models.Edition.objects.create(title="test")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=self.local_user.shelf_set.first(),
)
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
self.anonymous_user = AnonymousUser self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False self.anonymous_user.is_authenticated = False

View file

@ -1,4 +1,6 @@
""" test for app action functionality """ """ 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.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -21,9 +23,9 @@ class UserAdminViews(TestCase):
) )
models.SiteSettings.objects.create() 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 """ """ 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 = self.factory.get("")
request.user = self.local_user request.user = self.local_user
request.user.is_superuser = True request.user.is_superuser = True
@ -31,3 +33,38 @@ class UserAdminViews(TestCase):
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() result.render()
self.assertEqual(result.status_code, 200) 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"]
)

View file

@ -51,13 +51,20 @@ urlpatterns = [
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view() r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
), ),
# admin # 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( re_path(
r"^settings/email-preview", r"^settings/email-preview/?$",
views.site.email_preview, views.site.email_preview,
name="settings-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<user>\d+)/?$",
views.UserAdmin.as_view(),
name="settings-user",
),
re_path( re_path(
r"^settings/federation/?$", r"^settings/federation/?$",
views.Federation.as_view(), views.Federation.as_view(),
@ -113,9 +120,9 @@ urlpatterns = [
name="settings-report", name="settings-report",
), ),
re_path( re_path(
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$", r"^settings/reports/(?P<user_id>\d+)/suspend/?$",
views.deactivate_user, views.suspend_user,
name="settings-report-deactivate", name="settings-report-suspend",
), ),
re_path( re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$", r"^settings/reports/(?P<report_id>\d+)/resolve/?$",

View file

@ -25,7 +25,7 @@ from .notifications import Notifications
from .outbox import Outbox from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate 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 .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search from .search import Search
@ -37,5 +37,5 @@ from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag from .tag import Tag, AddTag, RemoveTag
from .updates import get_notification_count, get_unread_status_count from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following from .user import User, EditUser, Followers, Following
from .user_admin import UserAdmin from .user_admin import UserAdmin, UserAdminList
from .wellknown import * from .wellknown import *

View file

@ -270,10 +270,10 @@ class Editions(View):
if request.GET.get("format"): if request.GET.get("format"):
filters["physical_format__iexact"] = request.GET.get("format") filters["physical_format__iexact"] = request.GET.get("format")
editions = work.editions.order_by("-edition_rank").all() editions = work.editions.order_by("-edition_rank")
languages = set(sum([e.languages for e in editions], [])) languages = set(sum([e.languages for e in editions], []))
paginated = Paginator(editions.filter(**filters).all(), PAGE_LENGTH) paginated = Paginator(editions.filter(**filters), PAGE_LENGTH)
data = { data = {
"editions": paginated.get_page(request.GET.get("page")), "editions": paginated.get_page(request.GET.get("page")),
"work": work, "work": work,

View file

@ -74,12 +74,13 @@ class Report(View):
@login_required @login_required
@permission_required("bookwyrm_moderate_user") @permission_required("bookwyrm_moderate_user")
def deactivate_user(_, report_id): def suspend_user(_, user_id):
""" mark an account as inactive """ """ mark an account as inactive """
report = get_object_or_404(models.Report, id=report_id) user = get_object_or_404(models.User, id=user_id)
report.user.is_active = not report.user.is_active user.is_active = not user.is_active
report.user.save() # this isn't a full deletion, so we don't want to tell the world
return redirect("settings-report", report.id) user.save(broadcast=False)
return redirect("settings-user", user.id)
@login_required @login_required

View file

@ -59,7 +59,7 @@ class User(View):
{ {
"name": user_shelf.name, "name": user_shelf.name,
"local_path": user_shelf.local_path, "local_path": user_shelf.local_path,
"books": user_shelf.books[:3], "books": user_shelf.books.all()[:3],
"size": user_shelf.books.count(), "size": user_shelf.books.count(),
} }
) )

View file

@ -1,11 +1,12 @@
""" manage user """ """ manage user """
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from bookwyrm import models from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
@ -15,7 +16,7 @@ from bookwyrm.settings import PAGE_LENGTH
permission_required("bookwyrm.moderate_users", raise_exception=True), permission_required("bookwyrm.moderate_users", raise_exception=True),
name="dispatch", name="dispatch",
) )
class UserAdmin(View): class UserAdminList(View):
""" admin view of users on this server """ """ admin view of users on this server """
def get(self, request): def get(self, request):
@ -49,4 +50,28 @@ class UserAdmin(View):
"sort": sort, "sort": sort,
"server": server, "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)