mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-23 16:40:36 +00:00
Merge branch 'main' into draft-caching
This commit is contained in:
commit
59ec45496c
36 changed files with 533 additions and 36 deletions
|
@ -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
|
||||||
|
|
45
bookwyrm/migrations/0090_auto_20210908_2346.py
Normal file
45
bookwyrm/migrations/0090_auto_20210908_2346.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
bookwyrm/migrations/0090_emailblocklist.py
Normal file
32
bookwyrm/migrations/0090_emailblocklist.py
Normal 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",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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 = []
|
|
@ -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)
|
||||||
|
|
|
@ -13,6 +13,7 @@ DeactivationReason = models.TextChoices(
|
||||||
[
|
[
|
||||||
"pending",
|
"pending",
|
||||||
"self_deletion",
|
"self_deletion",
|
||||||
|
"moderator_suspension",
|
||||||
"moderator_deletion",
|
"moderator_deletion",
|
||||||
"domain_block",
|
"domain_block",
|
||||||
],
|
],
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
30
bookwyrm/templates/settings/domain_form.html
Normal file
30
bookwyrm/templates/settings/domain_form.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
62
bookwyrm/templates/settings/email_blocklist.html
Normal file
62
bookwyrm/templates/settings/email_blocklist.html
Normal 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 %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'settings/admin_layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
20
bookwyrm/templates/user_admin/delete_user_form.html
Normal file
20
bookwyrm/templates/user_admin/delete_user_form.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
73
bookwyrm/tests/views/test_email_blocks.py
Normal file
73
bookwyrm/tests/views/test_email_blocks.py
Normal 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()
|
||||||
|
)
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]):
|
||||||
|
|
49
bookwyrm/views/email_blocklist.py
Normal file
49
bookwyrm/views/email_blocklist.py
Normal 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")
|
|
@ -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"]
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue