Merge branch 'main' into draft-caching

This commit is contained in:
Mouse Reeve 2021-09-08 18:55:47 -07:00
commit 59ec45496c
36 changed files with 533 additions and 36 deletions

View file

@ -299,6 +299,12 @@ class ReportForm(CustomForm):
fields = ["user", "reporter", "statuses", "note"] fields = ["user", "reporter", "statuses", "note"]
class EmailBlocklistForm(CustomForm):
class Meta:
model = models.EmailBlocklist
fields = ["domain"]
class ServerForm(CustomForm): class ServerForm(CustomForm):
class Meta: class Meta:
model = models.FederatedServer model = models.FederatedServer

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.4 on 2021-09-08 23:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0089_user_show_suggested_users"),
]
operations = [
migrations.AlterField(
model_name="connector",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("pending", "Pending"),
("self_deletion", "Self Deletion"),
("moderator_suspension", "Moderator Suspension"),
("moderator_deletion", "Moderator Deletion"),
("domain_block", "Domain Block"),
],
max_length=255,
null=True,
),
),
migrations.AlterField(
model_name="user",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("pending", "Pending"),
("self_deletion", "Self Deletion"),
("moderator_suspension", "Moderator Suspension"),
("moderator_deletion", "Moderator Deletion"),
("domain_block", "Domain Block"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.4 on 2021-09-08 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0089_user_show_suggested_users"),
]
operations = [
migrations.CreateModel(
name="EmailBlocklist",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("domain", models.CharField(max_length=255, unique=True)),
],
options={
"ordering": ("-created_date",),
},
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.4 on 2021-09-09 00:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0090_auto_20210908_2346"),
("bookwyrm", "0090_emailblocklist"),
]
operations = []

View file

@ -24,7 +24,9 @@ from .federated_server import FederatedServer
from .import_job import ImportJob, ImportItem from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest from .site import SiteSettings, SiteInvite
from .site import PasswordReset, InviteRequest
from .site import EmailBlocklist
from .announcement import Announcement from .announcement import Announcement
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)

View file

@ -13,6 +13,7 @@ DeactivationReason = models.TextChoices(
[ [
"pending", "pending",
"self_deletion", "self_deletion",
"moderator_suspension",
"moderator_deletion", "moderator_deletion",
"domain_block", "domain_block",
], ],

View file

@ -123,6 +123,23 @@ class PasswordReset(models.Model):
return "https://{}/password-reset/{}".format(DOMAIN, self.code) return "https://{}/password-reset/{}".format(DOMAIN, self.code)
class EmailBlocklist(models.Model):
"""blocked email addresses"""
created_date = models.DateTimeField(auto_now_add=True)
domain = models.CharField(max_length=255, unique=True)
class Meta:
"""default sorting"""
ordering = ("-created_date",)
@property
def users(self):
"""find the users associated with this address"""
return User.objects.filter(email__endswith=f"@{self.domain}")
# pylint: disable=unused-argument # pylint: disable=unused-argument
@receiver(models.signals.post_save, sender=SiteSettings) @receiver(models.signals.post_save, sender=SiteSettings)
def preview_image(instance, *args, **kwargs): def preview_image(instance, *args, **kwargs):

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %}{% load humanize %} {% load i18n %}{% load humanize %}
{% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %} {% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %}

View file

@ -13,21 +13,21 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}"> <input type="hidden" name="user" value="{{ request.user.id }}">
<p> <p>
<label class="label" for="id_preview">Preview:</label> <label class="label" for="id_preview">{% trans "Preview:" %}</label>
{{ form.preview }} {{ form.preview }}
{% for error in form.preview.errors %} {% for error in form.preview.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</p> </p>
<p> <p>
<label class="label" for="id_content">Content:</label> <label class="label" for="id_content">{% trans "Content:" %}</label>
{{ form.content }} {{ form.content }}
{% for error in form.content.errors %} {% for error in form.content.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</p> </p>
<p> <p>
<label class="label" for="id_event_date">Event date:</label> <label class="label" for="id_event_date">{% trans "Event date:" %}</label>
<input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date"> <input type="date" name="event_date" value="{{ form.event_date.value|date:'Y-m-d' }}" class="input" id="id_event_date">
{% for error in form.event_date.errors %} {% for error in form.event_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -37,7 +37,7 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p> <p>
<label class="label" for="id_start_date">Start date:</label> <label class="label" for="id_start_date">{% trans "Start date:" %}</label>
<input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date"> <input type="date" name="start_date" class="input" value="{{ form.start_date.value|date:'Y-m-d' }}" id="id_start_date">
{% for error in form.start_date.errors %} {% for error in form.start_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -46,7 +46,7 @@
</div> </div>
<div class="column"> <div class="column">
<p> <p>
<label class="label" for="id_end_date">End date:</label> <label class="label" for="id_end_date">{% trans "End date:" %}</label>
<input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}"> <input type="date" name="end_date" class="input" id="id_end_date" value="{{ form.end_date.value|date:'Y-m-d' }}">
{% for error in form.end_date.errors %} {% for error in form.end_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -55,7 +55,7 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<p> <p>
<label class="label" for="id_active">Active:</label> <label class="label" for="id_active">{% trans "Active:" %}</label>
{{ form.active }} {{ form.active }}
{% for error in form.active.errors %} {% for error in form.active.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %}{% load humanize %} {% load i18n %}{% load humanize %}
{% block title %}{% trans "Announcements" %}{% endblock %} {% block title %}{% trans "Announcements" %}{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Add domain" %}
{% endblock %}
{% block form %}
<form name="add-domain" method="post" action="{% url 'settings-email-blocks' %}">
{% csrf_token %}
<label class="label" for="id_event_date">{% trans "Domain:" %}</label>
<div class="field has-addons">
<div class="control">
<div class="button is-disabled">@</div>
</div>
<div class="control">
{{ form.domain }}
</div>
</div>
{% for error in form.domain.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<div class="field">
<div class="control">
<button type="submit" class="button is-primary">{% trans "Add" %}</button>
</div>
</div>
</form>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add instance" %}{% endblock %} {% block title %}{% trans "Add instance" %}{% endblock %}

View file

@ -0,0 +1,62 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load humanize %}
{% block title %}{% trans "Email Blocklist" %}{% endblock %}
{% block header %}{% trans "Email Blocklist" %}{% endblock %}
{% block edit-button %}
{% trans "Add domain" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="add_domain" icon_with_text="plus" text=button_text focus="add_domain_header" %}
{% endblock %}
{% block panel %}
{% include 'settings/domain_form.html' with controls_text="add_domain" class="block" %}
<p class="notification block">
{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %}
</p>
<table class="table is-striped is-fullwidth">
<tr>
{% url 'settings-federation' as url %}
<th>
{% trans "Domain" as text %}
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
</th>
<th></th>
<th>
{% trans "Options" %}
</th>
</tr>
{% for domain in domains %}
<tr>
<td>{{ domain.domain }}</td>
<td>
<a href="{% url 'settings-users' %}?email=@{{ domain.domain }}">
{% with user_count=domain.users.count %}
{% blocktrans trimmed count conter=user_count with display_count=user_count|intcomma %}
{{ display_count }} user
{% plural %}
{{ display_count }} users
{% endblocktrans %}
{% endwith %}
</a>
</td>
<td>
<form name="remove-{{ domain.id }}" action="{% url 'settings-email-blocks-delete' domain.id %}" method="post">
{% csrf_token %}
{% trans "Delete" as button_text %}
<button class="button" type="submit">
<span class="icon icon-x" title="{{ button_text }}" aria-hidden="true"></span>
<span class="is-hidden-mobile">{{ button_text }}</span>
</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load markdown %} {% load markdown %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Federated Instances" %}{% endblock %} {% block title %}{% trans "Federated Instances" %}{% endblock %}

View file

@ -32,12 +32,6 @@
{% url 'settings-invites' as alt_url %} {% url 'settings-invites' as alt_url %}
<a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a> <a href="{{ url }}"{% if url in request.path or request.path in alt_url %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
</li> </li>
{% if perms.bookwyrm.moderate_user %}
<li>
{% url 'settings-reports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
</li>
{% endif %}
{% if perms.bookwyrm.control_federation %} {% if perms.bookwyrm.control_federation %}
<li> <li>
{% url 'settings-federation' as url %} {% url 'settings-federation' as url %}
@ -46,6 +40,19 @@
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
{% if perms.bookwyrm.moderate_user %}
<h2 class="menu-label">{% trans "Moderation" %}</h2>
<ul class="menu-list">
<li>
{% url 'settings-reports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
</li>
<li>
{% url 'settings-email-blocks' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Blocklist" %}</a>
</li>
</ul>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %} {% if perms.bookwyrm.edit_instance_settings %}
<h2 class="menu-label">{% trans "Instance Settings" %}</h2> <h2 class="menu-label">{% trans "Instance Settings" %}</h2>
<ul class="menu-list"> <ul class="menu-list">

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% block header %}{% trans "Invite Requests" %}{% endblock %} {% block header %}{% trans "Invite Requests" %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block header %}{% trans "Invites" %}{% endblock %} {% block header %}{% trans "Invites" %}{% endblock %}
{% load humanize %} {% load humanize %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add instance" %}{% endblock %} {% block title %}{% trans "Add instance" %}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Site Settings" %}{% endblock %} {% block title %}{% trans "Site Settings" %}{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends "components/inline_form.html" %}
{% load i18n %}
{% block header %}
{% trans "Permanently delete user" %}
{% endblock %}
{% block form %}
<form name="delete-user" action="{% url 'settings-delete-user' user.id %}" method="post">
{% csrf_token %}
<div class="field">
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required>
{% for error in form.password.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
</form>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{{ user.username }}{% endblock %} {% block title %}{{ user.username }}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Users" %}{% endblock %} {% block title %}{% trans "Users" %}{% endblock %}

View file

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<div class="block content"> <div class="block content">
{% if not user.is_active and user.deactivation_reason == "self_deletion" %} {% if not user.is_active and user.deactivation_reason == "self_deletion" or user.deactivation_reason == "moderator_deletion" %}
<div class="notification is-danger"> <div class="notification is-danger">
{% trans "Permanently deleted" %} {% trans "Permanently deleted" %}
</div> </div>
@ -8,18 +8,37 @@
<h3>{% trans "Actions" %}</h3> <h3>{% trans "Actions" %}</h3>
<div class="is-flex"> <div class="is-flex">
{% if user.is_active %}
<p class="mr-1"> <p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a> <a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p> </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 %} {% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form> </form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% endif %}
{% if user.local %}
<div>
{% trans "Permanently delete user" as button_text %}
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %}
</div> </div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "user_admin/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %} {% if user.local %}
<div> <div>

View file

@ -0,0 +1,73 @@
""" 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
from bookwyrm import models, views
class EmailBlocklistViews(TestCase):
"""every response to a get request, html or json"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
models.SiteSettings.objects.create()
def test_blocklist_page_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.EmailBlocklist.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_blocklist_page_post(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.EmailBlocklist.as_view()
request = self.factory.post("", {"domain": "gmail.com"})
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
self.assertTrue(
models.EmailBlocklist.objects.filter(domain="gmail.com").exists()
)
def test_blocklist_page_delete(self):
"""there are so many views, this just makes sure it LOADS"""
domain = models.EmailBlocklist.objects.create(domain="gmail.com")
view = views.EmailBlocklist.as_view()
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request, domain_id=domain.id)
self.assertEqual(result.status_code, 302)
self.assertFalse(
models.EmailBlocklist.objects.filter(domain="gmail.com").exists()
)

View file

@ -153,6 +153,30 @@ class RegisterViews(TestCase):
with self.assertRaises(PermissionDenied): with self.assertRaises(PermissionDenied):
view(request) view(request)
def test_register_blocked_domain(self, *_):
"""you can't register with a blocked domain"""
view = views.Register.as_view()
models.EmailBlocklist.objects.create(domain="gmail.com")
# one that fails
request = self.factory.post(
"register/",
{"localname": "nutria ", "password": "mouseword", "email": "aa@gmail.com"},
)
result = view(request)
self.assertEqual(result.status_code, 302)
self.assertFalse(models.User.objects.filter(email="aa@gmail.com").exists())
# one that succeeds
request = self.factory.post(
"register/",
{"localname": "nutria ", "password": "mouseword", "email": "aa@bleep.com"},
)
with patch("bookwyrm.views.register.login"):
result = view(request)
self.assertEqual(result.status_code, 302)
self.assertTrue(models.User.objects.filter(email="aa@bleep.com").exists())
def test_register_invite(self, *_): def test_register_invite(self, *_):
"""you can't just register""" """you can't just register"""
view = views.Register.as_view() view = views.Register.as_view()

View file

@ -1,4 +1,5 @@
""" test for app action functionality """ """ test for app action functionality """
import json
from unittest.mock import patch 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
@ -134,6 +135,26 @@ class ReportViews(TestCase):
self.assertFalse(self.rat.is_active) self.assertFalse(self.rat.is_active)
# re-activate # re-activate
views.suspend_user(request, self.rat.id) views.unsuspend_user(request, self.rat.id)
self.rat.refresh_from_db() self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active) self.assertTrue(self.rat.is_active)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_delete_user(self, *_):
"""toggle whether a user is able to log in"""
self.assertTrue(self.rat.is_active)
request = self.factory.post("", {"password": "password"})
request.user = self.local_user
request.user.is_superuser = True
# de-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.moderator_delete_user(request, self.rat.id)
self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Delete")
self.rat.refresh_from_db()
self.assertFalse(self.rat.is_active)
self.assertEqual(self.rat.deactivation_reason, "moderator_deletion")

View file

@ -141,6 +141,16 @@ urlpatterns = [
r"^invite-request/?$", views.InviteRequest.as_view(), name="invite-request" r"^invite-request/?$", views.InviteRequest.as_view(), name="invite-request"
), ),
re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()), re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
re_path(
r"^settings/email-blocklist/?$",
views.EmailBlocklist.as_view(),
name="settings-email-blocks",
),
re_path(
r"^settings/email-blocks/(?P<domain_id>\d+)/delete/?$",
views.EmailBlocklist.as_view(),
name="settings-email-blocks-delete",
),
# moderation # moderation
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"), re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
re_path( re_path(
@ -153,6 +163,16 @@ urlpatterns = [
views.suspend_user, views.suspend_user,
name="settings-report-suspend", name="settings-report-suspend",
), ),
re_path(
r"^settings/reports/(?P<user_id>\d+)/unsuspend/?$",
views.unsuspend_user,
name="settings-report-unsuspend",
),
re_path(
r"^settings/reports/(?P<user_id>\d+)/delete/?$",
views.moderator_delete_user,
name="settings-delete-user",
),
re_path( re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$", r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
views.resolve_report, views.resolve_report,

View file

@ -8,6 +8,7 @@ from .directory import Directory
from .discover import Discover from .discover import Discover
from .edit_user import EditUser, DeleteUser from .edit_user import EditUser, DeleteUser
from .editions import Editions, switch_edition from .editions import Editions, switch_edition
from .email_blocklist import EmailBlocklist
from .federation import Federation, FederatedServer from .federation import Federation, FederatedServer
from .federation import AddFederatedServer, ImportServerBlocklist from .federation import AddFederatedServer, ImportServerBlocklist
from .federation import block_server, unblock_server from .federation import block_server, unblock_server
@ -33,7 +34,15 @@ from .reading import edit_readthrough, create_readthrough
from .reading import delete_readthrough, delete_progressupdate from .reading import delete_readthrough, delete_progressupdate
from .reading import ReadingStatus from .reading import ReadingStatus
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
from .reports import Report, Reports, make_report, resolve_report, suspend_user from .reports import (
Report,
Reports,
make_report,
resolve_report,
suspend_user,
unsuspend_user,
moderator_delete_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

View file

@ -55,7 +55,6 @@ class DeleteUser(View):
def post(self, request): def post(self, request):
"""les get fancy with images""" """les get fancy with images"""
form = forms.DeleteUserForm(request.POST, instance=request.user) form = forms.DeleteUserForm(request.POST, instance=request.user)
form.is_valid()
# idk why but I couldn't get check_password to work on request.user # idk why but I couldn't get check_password to work on request.user
user = models.User.objects.get(id=request.user.id) user = models.User.objects.get(id=request.user.id)
if form.is_valid() and user.check_password(form.cleaned_data["password"]): if form.is_valid() and user.check_password(form.cleaned_data["password"]):

View file

@ -0,0 +1,49 @@
""" moderation via flagged posts and users """
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import forms, models
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_user", raise_exception=True),
name="dispatch",
)
class EmailBlocklist(View):
"""Block users by email address"""
def get(self, request):
"""view and compose blocks"""
data = {
"domains": models.EmailBlocklist.objects.order_by("-created_date").all(),
"form": forms.EmailBlocklistForm(),
}
return TemplateResponse(request, "settings/email_blocklist.html", data)
def post(self, request, domain_id=None):
"""create a new domain block"""
if domain_id:
return self.delete(request, domain_id)
form = forms.EmailBlocklistForm(request.POST)
data = {
"domains": models.EmailBlocklist.objects.order_by("-created_date").all(),
"form": form,
}
if not form.is_valid():
return TemplateResponse(request, "settings/email_blocklist.html", data)
form.save()
data["form"] = forms.EmailBlocklistForm()
return TemplateResponse(request, "settings/email_blocklist.html", data)
# pylint: disable=unused-argument
def delete(self, request, domain_id):
"""remove a domain block"""
domain = get_object_or_404(models.EmailBlocklist, id=domain_id)
domain.delete()
return redirect("settings-email-blocks")

View file

@ -42,6 +42,12 @@ class Register(View):
email = form.data["email"] email = form.data["email"]
password = form.data["password"] password = form.data["password"]
# make sure the email isn't blocked as spam
email_domain = email.split("@")[-1]
if models.EmailBlocklist.objects.filter(domain=email_domain).exists():
# treat this like a successful registration, but don't do anything
return redirect("confirm-email")
# check localname and email uniqueness # check localname and email uniqueness
if models.User.objects.filter(localname=localname).first(): if models.User.objects.filter(localname=localname).first():
form.errors["localname"] = ["User with this username already exists"] form.errors["localname"] = ["User with this username already exists"]

View file

@ -1,5 +1,6 @@
""" moderation via flagged posts and users """ """ moderation via flagged posts and users """
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
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
@ -77,12 +78,50 @@ class Report(View):
def suspend_user(_, user_id): def suspend_user(_, user_id):
"""mark an account as inactive""" """mark an account as inactive"""
user = get_object_or_404(models.User, id=user_id) user = get_object_or_404(models.User, id=user_id)
user.is_active = not user.is_active user.is_active = False
user.deactivation_reason = "moderator_suspension"
# this isn't a full deletion, so we don't want to tell the world # this isn't a full deletion, so we don't want to tell the world
user.save(broadcast=False) user.save(broadcast=False)
return redirect("settings-user", user.id) return redirect("settings-user", user.id)
@login_required
@permission_required("bookwyrm_moderate_user")
def unsuspend_user(_, user_id):
"""mark an account as inactive"""
user = get_object_or_404(models.User, id=user_id)
user.is_active = True
user.deactivation_reason = None
# 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
@permission_required("bookwyrm_moderate_user")
def moderator_delete_user(request, user_id):
"""permanently delete a user"""
user = get_object_or_404(models.User, id=user_id)
# we can't delete users on other instances
if not user.local:
raise PermissionDenied
form = forms.DeleteUserForm(request.POST, instance=user)
moderator = models.User.objects.get(id=request.user.id)
# check the moderator's password
if form.is_valid() and moderator.check_password(form.cleaned_data["password"]):
user.deactivation_reason = "moderator_deletion"
user.delete()
return redirect("settings-user", user.id)
form.errors["password"] = ["Invalid password"]
data = {"user": user, "group_form": forms.UserGroupForm(), "form": form}
return TemplateResponse(request, "user_admin/user.html", data)
@login_required @login_required
@permission_required("bookwyrm_moderate_post") @permission_required("bookwyrm_moderate_post")
def resolve_report(_, report_id): def resolve_report(_, report_id):

View file

@ -33,6 +33,9 @@ class UserAdminList(View):
scope = request.GET.get("scope") scope = request.GET.get("scope")
if scope and scope == "local": if scope and scope == "local":
filters["local"] = True filters["local"] = True
email = request.GET.get("email")
if email:
filters["email__endswith"] = email
users = models.User.objects.filter(**filters) users = models.User.objects.filter(**filters)