Merge pull request #1974 from bookwyrm-social/celerybeat

Schedules automod tasks
This commit is contained in:
Mouse Reeve 2022-03-16 12:54:06 -07:00 committed by GitHub
commit f4e828e2fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 14 deletions

View file

@ -5,6 +5,7 @@ from django import forms
from django.forms import widgets from django.forms import widgets
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import IntervalSchedule
from bookwyrm import models from bookwyrm import models
from .custom_form import CustomForm from .custom_form import CustomForm
@ -127,3 +128,14 @@ class AutoModRuleForm(CustomForm):
class Meta: class Meta:
model = models.AutoMod model = models.AutoMod
fields = ["string_match", "flag_users", "flag_statuses", "created_by"] fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
class IntervalScheduleForm(CustomForm):
class Meta:
model = IntervalSchedule
fields = ["every", "period"]
widgets = {
"every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}),
"period": forms.Select(attrs={"aria-describedby": "desc_period"}),
}

View file

@ -90,6 +90,7 @@ INSTALLED_APPS = [
"sass_processor", "sass_processor",
"bookwyrm", "bookwyrm",
"celery", "celery",
"django_celery_beat",
"imagekit", "imagekit",
"storages", "storages",
] ]

View file

@ -1,5 +1,6 @@
{% extends 'settings/layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %}
{% load utilities %} {% load utilities %}
{% block title %} {% block title %}
@ -16,12 +17,81 @@
<p> <p>
{% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %} {% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %}
{% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %} {% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %}
{% trans "At this time, reports are <em>not</em> being generated automatically, and you must manually trigger a scan." %}
</p> </p>
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}"> </div>
<div class="box block">
{% if task %}
<dl class="block">
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Schedule:" %}
</dt>
<dd>
{{ task.schedule }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Last run:" %}
</dt>
<dd>
{{ task.last_run_at|naturaltime }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Total run count:" %}
</dt>
<dd>
{{ task.total_run_count }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Enabled:" %}
</dt>
<dd>
<span class="tag {% if task.enabled %}is-success{% else %}is-danger{% endif %}">
{{ task.enabled|yesno }}
</span>
</dd>
</dl>
<div class="is-flex is-justify-content-space-between block">
<form name="unschedule-scan" method="POST" action="{% url 'settings-automod-unschedule' task.id %}">
{% csrf_token %}
<button class="button is-danger">{% trans "Delete schedule" %}</button>
</form>
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
{% csrf_token %}
<button class="button">{% trans "Run now" %}</button>
<p class="help">{% trans "Last run date will not be updated" %}</p>
</form>
</div>
{% else %}
<h2 class="title is-4">{% trans "Schedule scan" %}</h2>
<form name="schedule-scan" method="POST" action="{% url 'settings-automod-schedule' %}">
{% csrf_token %} {% csrf_token %}
<button class="button is-warning">{% trans "Run scan" %}</button> <div class="field">
<label class="label" for="id_every">
{{ task_form.every.label }}
</label>
{{ task_form.every }}
<p class="help" id="desc_every">
{{ task_form.every.help_text }}
</p>
</div>
<div class="field">
<label class="label" for="id_period">
{{ task_form.period.label }}
</label>
<div class="select">
{{ task_form.period }}
</div>
<p class="help" id="desc_period">
{{ task_form.period.help_text }}
</p>
</div>
<button class="button is-warning">{% trans "Schedule scan" %}</button>
</form> </form>
{% endif %}
</div> </div>
{% if success %} {% if success %}

View file

@ -233,11 +233,23 @@ urlpatterns = [
# auto-moderation rules # auto-moderation rules
re_path(r"^settings/automod/?$", views.AutoMod.as_view(), name="settings-automod"), re_path(r"^settings/automod/?$", views.AutoMod.as_view(), name="settings-automod"),
re_path( re_path(
r"^settings/automod/(?P<rule_id>\d+)/delete?$", r"^settings/automod/(?P<rule_id>\d+)/delete/?$",
views.automod_delete, views.automod_delete,
name="settings-automod-delete", name="settings-automod-delete",
), ),
re_path(r"^settings/automod/run?$", views.run_automod, name="settings-automod-run"), re_path(
r"^settings/automod/schedule/?$",
views.schedule_automod_task,
name="settings-automod-schedule",
),
re_path(
r"^settings/automod/unschedule/(?P<task_id>\d+)/?$",
views.unschedule_automod_task,
name="settings-automod-unschedule",
),
re_path(
r"^settings/automod/run/?$", views.run_automod, name="settings-automod-run"
),
# moderation # moderation
re_path( re_path(
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports" r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"

View file

@ -3,6 +3,7 @@
from .admin.announcements import Announcements, Announcement from .admin.announcements import Announcements, Announcement
from .admin.announcements import EditAnnouncement, delete_announcement from .admin.announcements import EditAnnouncement, delete_announcement
from .admin.automod import AutoMod, automod_delete, run_automod from .admin.automod import AutoMod, automod_delete, run_automod
from .admin.automod import schedule_automod_task, unschedule_automod_task
from .admin.dashboard import Dashboard from .admin.dashboard import Dashboard
from .admin.federation import Federation, FederatedServer from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist from .admin.federation import AddFederatedServer, ImportServerBlocklist

View file

@ -1,10 +1,12 @@
""" 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.db import transaction
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
from django.views import View from django.views import View
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django_celery_beat.models import PeriodicTask
from bookwyrm import forms, models from bookwyrm import forms, models
@ -24,8 +26,9 @@ class AutoMod(View):
def get(self, request): def get(self, request):
"""view rules""" """view rules"""
data = {"rules": models.AutoMod.objects.all(), "form": forms.AutoModRuleForm()} return TemplateResponse(
return TemplateResponse(request, "settings/automod/rules.html", data) request, "settings/automod/rules.html", automod_view_data()
)
def post(self, request): def post(self, request):
"""add rule""" """add rule"""
@ -35,22 +38,49 @@ class AutoMod(View):
form.save() form.save()
form = forms.AutoModRuleForm() form = forms.AutoModRuleForm()
data = { data = automod_view_data()
"rules": models.AutoMod.objects.all(), data["form"] = form
"form": form,
"success": success,
}
return TemplateResponse(request, "settings/automod/rules.html", data) return TemplateResponse(request, "settings/automod/rules.html", data)
@require_POST
@permission_required("bookwyrm.moderate_user", raise_exception=True)
@permission_required("bookwyrm.moderate_post", raise_exception=True)
def schedule_automod_task(request):
"""scheduler"""
form = forms.IntervalScheduleForm(request.POST)
if not form.is_valid():
data = automod_view_data()
data["task_form"] = form
return TemplateResponse(request, "settings/automod/rules.html", data)
with transaction.atomic():
schedule = form.save()
PeriodicTask.objects.get_or_create(
interval=schedule,
name="automod-task",
task="bookwyrm.models.antispam.automod_task",
)
return redirect("settings-automod")
@require_POST
@permission_required("bookwyrm.moderate_user", raise_exception=True)
@permission_required("bookwyrm.moderate_post", raise_exception=True)
# pylint: disable=unused-argument
def unschedule_automod_task(request, task_id):
"""unscheduler"""
get_object_or_404(PeriodicTask, id=task_id).delete()
return redirect("settings-automod")
@require_POST @require_POST
@permission_required("bookwyrm.moderate_user", raise_exception=True) @permission_required("bookwyrm.moderate_user", raise_exception=True)
@permission_required("bookwyrm.moderate_post", raise_exception=True) @permission_required("bookwyrm.moderate_post", raise_exception=True)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def automod_delete(request, rule_id): def automod_delete(request, rule_id):
"""Remove a rule""" """Remove a rule"""
rule = get_object_or_404(models.AutoMod, id=rule_id) get_object_or_404(models.AutoMod, id=rule_id).delete()
rule.delete()
return redirect("settings-automod") return redirect("settings-automod")
@ -62,3 +92,18 @@ def run_automod(request):
"""run scan""" """run scan"""
models.automod_task.delay() models.automod_task.delay()
return redirect("settings-automod") return redirect("settings-automod")
def automod_view_data():
"""helper to get data used in the template"""
try:
task = PeriodicTask.objects.get(name="automod-task")
except PeriodicTask.DoesNotExist:
task = None
return {
"task": task,
"task_form": forms.IntervalScheduleForm(),
"rules": models.AutoMod.objects.all(),
"form": forms.AutoModRuleForm(),
}

1
bw-dev
View file

@ -215,6 +215,7 @@ case "$CMD" in
;; ;;
setup) setup)
migrate migrate
migrate django_celery_beat
initdb initdb
runweb python manage.py collectstatic --no-input runweb python manage.py collectstatic --no-input
admin_code admin_code

View file

@ -3,6 +3,7 @@
# pylint: disable=unused-wildcard-import # pylint: disable=unused-wildcard-import
from bookwyrm.settings import * from bookwyrm.settings import *
# pylint: disable=line-too-long
REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", None)) REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", None))
REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker") REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker")
REDIS_BROKER_PORT = env("REDIS_BROKER_PORT", 6379) REDIS_BROKER_PORT = env("REDIS_BROKER_PORT", 6379)
@ -16,6 +17,10 @@ CELERY_DEFAULT_QUEUE = "low_priority"
CELERY_ACCEPT_CONTENT = ["json"] CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json" CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json"
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
CELERY_TIMEZONE = env("TIME_ZONE", "UTC")
FLOWER_PORT = env("FLOWER_PORT") FLOWER_PORT = env("FLOWER_PORT")
INSTALLED_APPS = INSTALLED_APPS + [ INSTALLED_APPS = INSTALLED_APPS + [

View file

@ -70,6 +70,19 @@ services:
- db - db
- redis_broker - redis_broker
restart: on-failure restart: on-failure
celery_beat:
env_file: .env
build: .
networks:
- main
command: celery -A celerywyrm beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler
volumes:
- .:/app
- static_volume:/app/static
- media_volume:/app/images
depends_on:
- celery_worker
restart: on-failure
flower: flower:
build: . build: .
command: celery -A celerywyrm flower --basic_auth=${FLOWER_USER}:${FLOWER_PASSWORD} command: celery -A celerywyrm flower --basic_auth=${FLOWER_USER}:${FLOWER_PASSWORD}

View file

@ -1,6 +1,7 @@
celery==5.2.2 celery==5.2.2
colorthief==0.2.1 colorthief==0.2.1
Django==3.2.12 Django==3.2.12
django-celery-beat==2.2.1
django-compressor==2.4.1 django-compressor==2.4.1
django-imagekit==4.1.0 django-imagekit==4.1.0
django-model-utils==4.0.0 django-model-utils==4.0.0