Resolve "Screening for signups"
This commit is contained in:
parent
e6df21b96c
commit
e313fcd033
49 changed files with 1759 additions and 49 deletions
|
|
@ -1,7 +1,11 @@
|
|||
from dynamic_preferences import types
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.common import preferences as common_preferences
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
|
||||
from . import models
|
||||
|
||||
|
|
@ -40,3 +44,52 @@ class UnauthenticatedReportTypes(common_preferences.StringListPreference):
|
|||
help_text = "A list of categories for which external users (without an account) can submit a report"
|
||||
choices = models.REPORT_TYPES
|
||||
field_kwargs = {"choices": choices, "required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class SignupApprovalEnabled(types.BooleanPreference):
|
||||
show_in_api = True
|
||||
section = moderation
|
||||
name = "signup_approval_enabled"
|
||||
verbose_name = "Enable manual sign-up validation"
|
||||
help_text = "If enabled, new registrations will go to a moderation queue and need to be reviewed by moderators."
|
||||
default = False
|
||||
|
||||
|
||||
CUSTOM_FIELDS_TYPES = [
|
||||
"short_text",
|
||||
"long_text",
|
||||
]
|
||||
|
||||
|
||||
class CustomFieldSerializer(serializers.Serializer):
|
||||
label = serializers.CharField()
|
||||
required = serializers.BooleanField(default=True)
|
||||
input_type = serializers.ChoiceField(choices=CUSTOM_FIELDS_TYPES)
|
||||
|
||||
|
||||
class CustomFormSerializer(serializers.Serializer):
|
||||
help_text = common_serializers.ContentSerializer(required=False, allow_null=True)
|
||||
fields = serializers.ListField(
|
||||
child=CustomFieldSerializer(), min_length=0, max_length=10, required=False
|
||||
)
|
||||
|
||||
def validate_help_text(self, v):
|
||||
if not v:
|
||||
return
|
||||
v["html"] = common_utils.render_html(
|
||||
v["text"], content_type=v["content_type"], permissive=True
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class SignupFormCustomization(common_preferences.SerializedPreference):
|
||||
show_in_api = True
|
||||
section = moderation
|
||||
name = "signup_form_customization"
|
||||
verbose_name = "Sign-up form customization"
|
||||
help_text = "Configure custom fields and help text for your sign-up form"
|
||||
required = False
|
||||
default = {}
|
||||
data_serializer_class = CustomFormSerializer
|
||||
|
|
|
|||
|
|
@ -74,3 +74,20 @@ class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
|||
return
|
||||
|
||||
self.target_owner = serializers.get_target_owner(self.target)
|
||||
|
||||
|
||||
@registry.register
|
||||
class UserRequestFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||
submitter = factory.SubFactory(federation_factories.ActorFactory, local=True)
|
||||
|
||||
class Meta:
|
||||
model = "moderation.UserRequest"
|
||||
|
||||
class Params:
|
||||
signup = factory.Trait(
|
||||
submitter=factory.SubFactory(federation_factories.ActorFactory, local=True),
|
||||
type="signup",
|
||||
)
|
||||
assigned = factory.Trait(
|
||||
assigned_to=factory.SubFactory(federation_factories.ActorFactory)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 3.0.4 on 2020-03-17 08:20
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('federation', '0025_auto_20200317_0820'),
|
||||
('moderation', '0004_note'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='summary',
|
||||
field=models.TextField(blank=True, max_length=50000, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField(blank=True, max_length=500, null=True)),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('handled_date', models.DateTimeField(null=True)),
|
||||
('type', models.CharField(choices=[('signup', 'Sign-up')], max_length=40)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('refused', 'Refused'), ('approved', 'approved')], default='pending', max_length=40)),
|
||||
('metadata', django.contrib.postgres.fields.jsonb.JSONField(null=True)),
|
||||
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_requests', to='federation.Actor')),
|
||||
('submitter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='federation.Actor')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -185,6 +185,43 @@ class Note(models.Model):
|
|||
target = GenericForeignKey("target_content_type", "target_id")
|
||||
|
||||
|
||||
USER_REQUEST_TYPES = [
|
||||
("signup", "Sign-up"),
|
||||
]
|
||||
|
||||
USER_REQUEST_STATUSES = [
|
||||
("pending", "Pending"),
|
||||
("refused", "Refused"),
|
||||
("approved", "Approved"),
|
||||
]
|
||||
|
||||
|
||||
class UserRequest(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
handled_date = models.DateTimeField(null=True)
|
||||
type = models.CharField(max_length=40, choices=USER_REQUEST_TYPES)
|
||||
status = models.CharField(
|
||||
max_length=40, choices=USER_REQUEST_STATUSES, default="pending"
|
||||
)
|
||||
submitter = models.ForeignKey(
|
||||
"federation.Actor", related_name="requests", on_delete=models.CASCADE,
|
||||
)
|
||||
assigned_to = models.ForeignKey(
|
||||
"federation.Actor",
|
||||
related_name="assigned_requests",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
metadata = JSONField(null=True)
|
||||
|
||||
notes = GenericRelation(
|
||||
"Note", content_type_field="target_content_type", object_id_field="target_id"
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Report)
|
||||
def set_handled_date(sender, instance, **kwargs):
|
||||
if instance.is_handled is True and not instance.handled_date:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import logging
|
||||
from django.core import mail
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
|
||||
from funkwhale_api.common import channels
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.common import utils
|
||||
from funkwhale_api.taskapp import celery
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
|
|
@ -41,11 +43,7 @@ def trigger_moderator_email(report, **kwargs):
|
|||
utils.on_commit(send_new_report_email_to_moderators.delay, report_id=report.pk)
|
||||
|
||||
|
||||
@celery.app.task(name="moderation.send_new_report_email_to_moderators")
|
||||
@celery.require_instance(
|
||||
models.Report.objects.select_related("submitter").filter(is_handled=False), "report"
|
||||
)
|
||||
def send_new_report_email_to_moderators(report):
|
||||
def get_moderators():
|
||||
moderators = users_models.User.objects.filter(
|
||||
is_active=True, permission_moderation=True
|
||||
)
|
||||
|
|
@ -53,6 +51,15 @@ def send_new_report_email_to_moderators(report):
|
|||
# we fallback on superusers
|
||||
moderators = users_models.User.objects.filter(is_superuser=True)
|
||||
moderators = sorted(moderators, key=lambda m: m.pk)
|
||||
return moderators
|
||||
|
||||
|
||||
@celery.app.task(name="moderation.send_new_report_email_to_moderators")
|
||||
@celery.require_instance(
|
||||
models.Report.objects.select_related("submitter").filter(is_handled=False), "report"
|
||||
)
|
||||
def send_new_report_email_to_moderators(report):
|
||||
moderators = get_moderators()
|
||||
submitter_repr = (
|
||||
report.submitter.full_username if report.submitter else report.submitter_email
|
||||
)
|
||||
|
|
@ -114,3 +121,148 @@ def send_new_report_email_to_moderators(report):
|
|||
recipient_list=[moderator.email],
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
||||
|
||||
@celery.app.task(name="moderation.user_request_handle")
|
||||
@celery.require_instance(
|
||||
models.UserRequest.objects.select_related("submitter"), "user_request"
|
||||
)
|
||||
@transaction.atomic
|
||||
def user_request_handle(user_request, new_status, old_status=None):
|
||||
if user_request.status != new_status:
|
||||
logger.warn(
|
||||
"User request %s was handled before asynchronous tasks run", user_request.pk
|
||||
)
|
||||
return
|
||||
|
||||
if user_request.type == "signup" and new_status == "pending" and old_status is None:
|
||||
notify_mods_signup_request_pending(user_request)
|
||||
broadcast_user_request_created(user_request)
|
||||
elif user_request.type == "signup" and new_status == "approved":
|
||||
user_request.submitter.user.is_active = True
|
||||
user_request.submitter.user.save(update_fields=["is_active"])
|
||||
notify_submitter_signup_request_approved(user_request)
|
||||
elif user_request.type == "signup" and new_status == "refused":
|
||||
notify_submitter_signup_request_refused(user_request)
|
||||
|
||||
|
||||
def broadcast_user_request_created(user_request):
|
||||
from funkwhale_api.manage import serializers as manage_serializers
|
||||
|
||||
channels.group_send(
|
||||
"admin.moderation",
|
||||
{
|
||||
"type": "event.send",
|
||||
"text": "",
|
||||
"data": {
|
||||
"type": "user_request.created",
|
||||
"user_request": manage_serializers.ManageUserRequestSerializer(
|
||||
user_request
|
||||
).data,
|
||||
"pending_count": models.UserRequest.objects.filter(
|
||||
status="pending"
|
||||
).count(),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def notify_mods_signup_request_pending(obj):
|
||||
moderators = get_moderators()
|
||||
submitter_repr = obj.submitter.preferred_username
|
||||
subject = "[{} moderation] New sign-up request from {}".format(
|
||||
settings.FUNKWHALE_HOSTNAME, submitter_repr
|
||||
)
|
||||
detail_url = federation_utils.full_url(
|
||||
"/manage/moderation/requests/{}".format(obj.uuid)
|
||||
)
|
||||
unresolved_requests_url = federation_utils.full_url(
|
||||
"/manage/moderation/requests?q=status:pending"
|
||||
)
|
||||
unresolved_requests = models.UserRequest.objects.filter(status="pending").count()
|
||||
body = [
|
||||
"{} wants to register on your pod. You need to review their request before they can use the service.".format(
|
||||
submitter_repr
|
||||
),
|
||||
"",
|
||||
"- To handle this request, please visit {}".format(detail_url),
|
||||
"- To view all unresolved requests (currently {}), please visit {}".format(
|
||||
unresolved_requests, unresolved_requests_url
|
||||
),
|
||||
"",
|
||||
"—",
|
||||
"",
|
||||
"You are receiving this email because you are a moderator for {}.".format(
|
||||
settings.FUNKWHALE_HOSTNAME
|
||||
),
|
||||
]
|
||||
|
||||
for moderator in moderators:
|
||||
if not moderator.email:
|
||||
logger.warning("Moderator %s has no email configured", moderator.username)
|
||||
continue
|
||||
mail.send_mail(
|
||||
subject,
|
||||
message="\n".join(body),
|
||||
recipient_list=[moderator.email],
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
||||
|
||||
def notify_submitter_signup_request_approved(user_request):
|
||||
submitter_repr = user_request.submitter.preferred_username
|
||||
submitter_email = user_request.submitter.user.email
|
||||
if not submitter_email:
|
||||
logger.warning("User %s has no email configured", submitter_repr)
|
||||
return
|
||||
subject = "Welcome to {}, {}!".format(settings.FUNKWHALE_HOSTNAME, submitter_repr)
|
||||
login_url = federation_utils.full_url("/login")
|
||||
body = [
|
||||
"Hi {} and welcome,".format(submitter_repr),
|
||||
"",
|
||||
"Our moderation team has approved your account request and you can now start "
|
||||
"using the service. Please visit {} to get started.".format(login_url),
|
||||
"",
|
||||
"Before your first login, you may need to verify your email address if you didn't already.",
|
||||
]
|
||||
|
||||
mail.send_mail(
|
||||
subject,
|
||||
message="\n".join(body),
|
||||
recipient_list=[submitter_email],
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
||||
|
||||
def notify_submitter_signup_request_refused(user_request):
|
||||
submitter_repr = user_request.submitter.preferred_username
|
||||
submitter_email = user_request.submitter.user.email
|
||||
if not submitter_email:
|
||||
logger.warning("User %s has no email configured", submitter_repr)
|
||||
return
|
||||
subject = "Your account request at {} was refused".format(
|
||||
settings.FUNKWHALE_HOSTNAME
|
||||
)
|
||||
body = [
|
||||
"Hi {},".format(submitter_repr),
|
||||
"",
|
||||
"You recently submitted an account request on our service. However, our "
|
||||
"moderation team has refused it, and as a result, you won't be able to use "
|
||||
"the service.",
|
||||
]
|
||||
|
||||
instance_contact_email = preferences.get("instance__contact_email")
|
||||
if instance_contact_email:
|
||||
body += [
|
||||
"",
|
||||
"If you think this is a mistake, please contact our team at {}.".format(
|
||||
instance_contact_email
|
||||
),
|
||||
]
|
||||
|
||||
mail.send_mail(
|
||||
subject,
|
||||
message="\n".join(body),
|
||||
recipient_list=[submitter_email],
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ NOTE_TARGET_FIELDS = {
|
|||
"id_attr": "uuid",
|
||||
"id_field": serializers.UUIDField(),
|
||||
},
|
||||
"request": {
|
||||
"queryset": models.UserRequest.objects.all(),
|
||||
"id_attr": "uuid",
|
||||
"id_field": serializers.UUIDField(),
|
||||
},
|
||||
"account": {
|
||||
"queryset": federation_models.Actor.objects.all(),
|
||||
"id_attr": "full_username",
|
||||
|
|
@ -19,3 +24,21 @@ NOTE_TARGET_FIELDS = {
|
|||
"get_query": moderation_serializers.get_actor_query,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_signup_form_additional_fields_serializer(customization):
|
||||
fields = (customization or {}).get("fields", []) or []
|
||||
|
||||
class AdditionalFieldsSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in fields:
|
||||
required = bool(field.get("required", True))
|
||||
self.fields[field["label"]] = serializers.CharField(
|
||||
max_length=5000,
|
||||
required=required,
|
||||
allow_null=not required,
|
||||
allow_blank=not required,
|
||||
)
|
||||
|
||||
return AdditionalFieldsSerializer(required=fields, allow_null=not fields)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue