From ddffbeadfa118d264331035a670377886e73ac79 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 5 Jan 2019 18:21:35 +0100 Subject: [PATCH 01/15] Can now add domains via manage API / UI --- api/funkwhale_api/common/validators.py | 15 ++++++ api/funkwhale_api/federation/models.py | 7 ++- api/funkwhale_api/manage/views.py | 5 +- api/tests/manage/test_serializers.py | 9 ++++ api/tests/manage/test_views.py | 9 ++++ .../views/admin/moderation/DomainsList.vue | 50 +++++++++++++++++-- 6 files changed, 90 insertions(+), 5 deletions(-) diff --git a/api/funkwhale_api/common/validators.py b/api/funkwhale_api/common/validators.py index b5f26cac5..78a4b4c7c 100644 --- a/api/funkwhale_api/common/validators.py +++ b/api/funkwhale_api/common/validators.py @@ -1,6 +1,7 @@ import mimetypes from os.path import splitext +from django.core import validators from django.core.exceptions import ValidationError from django.core.files.images import get_image_dimensions from django.template.defaultfilters import filesizeformat @@ -150,3 +151,17 @@ class FileValidator(object): } raise ValidationError(message) + + +class DomainValidator(validators.URLValidator): + message = "Enter a valid domain name." + + def __call__(self, value): + """ + This is a bit hackish but since we don't have any built-in domain validator, + we use the url one, and prepend http:// in front of it. + + If it fails, we know the domain is not valid. + """ + super().__call__("http://{}".format(value)) + return value diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 2fdeaaa76..59360aea1 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -13,6 +13,7 @@ from django.urls import reverse from funkwhale_api.common import session from funkwhale_api.common import utils as common_utils +from funkwhale_api.common import validators as common_validators from funkwhale_api.music import utils as music_utils from . import utils as federation_utils @@ -83,7 +84,11 @@ class DomainQuerySet(models.QuerySet): class Domain(models.Model): - name = models.CharField(primary_key=True, max_length=255) + name = models.CharField( + primary_key=True, + max_length=255, + validators=[common_validators.DomainValidator()], + ) creation_date = models.DateTimeField(default=timezone.now) nodeinfo_fetch_date = models.DateTimeField(default=None, null=True, blank=True) nodeinfo = JSONField(default=empty_dict, max_length=50000, blank=True) diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 0697c6c14..763b37497 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -98,7 +98,10 @@ class ManageInvitationViewSet( class ManageDomainViewSet( - mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, ): lookup_value_regex = r"[a-zA-Z0-9\-\.]+" queryset = ( diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 803820b48..74ba96ba8 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -1,3 +1,5 @@ +import pytest + from funkwhale_api.manage import serializers @@ -53,6 +55,13 @@ def test_manage_domain_serializer(factories, now): assert s.data == expected +def test_manage_domain_serializer_validates_hostname(db): + s = serializers.ManageDomainSerializer(data={"name": "hello world"}) + + with pytest.raises(serializers.serializers.ValidationError): + s.is_valid(raise_exception=True) + + def test_manage_actor_serializer(factories, now): actor = factories["federation.Actor"]() setattr(actor, "uploads_count", 66) diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index 72e945bca..4591f7b1b 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -1,6 +1,7 @@ import pytest from django.urls import reverse +from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.manage import serializers, views @@ -90,6 +91,14 @@ def test_domain_detail(factories, superuser_api_client): assert response.data["name"] == d.pk +def test_domain_create(superuser_api_client): + url = reverse("api:v1:manage:federation:domains-list") + response = superuser_api_client.post(url, {"name": "test.federation"}) + + assert response.status_code == 201 + assert federation_models.Domain.objects.filter(pk="test.federation").exists() + + def test_domain_nodeinfo(factories, superuser_api_client, mocker): domain = factories["federation.Domain"]() url = reverse( diff --git a/front/src/views/admin/moderation/DomainsList.vue b/front/src/views/admin/moderation/DomainsList.vue index 84fb1df43..259d05f0c 100644 --- a/front/src/views/admin/moderation/DomainsList.vue +++ b/front/src/views/admin/moderation/DomainsList.vue @@ -1,26 +1,70 @@ From 0bc9bb65b04ea4b5d3b98d386d790f65295a2c19 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 7 Jan 2019 09:45:53 +0100 Subject: [PATCH 02/15] Model, view and serializer for instance-level policies --- api/config/settings/common.py | 1 + api/funkwhale_api/federation/factories.py | 4 +- api/funkwhale_api/federation/fields.py | 18 ++++++ api/funkwhale_api/manage/filters.py | 22 +++++++ api/funkwhale_api/manage/serializers.py | 64 +++++++++++++++++++ api/funkwhale_api/manage/urls.py | 11 ++++ api/funkwhale_api/manage/views.py | 24 +++++++ api/funkwhale_api/moderation/__init__.py | 0 api/funkwhale_api/moderation/factories.py | 23 +++++++ .../moderation/migrations/0001_initial.py | 35 ++++++++++ .../moderation/migrations/__init__.py | 0 api/funkwhale_api/moderation/models.py | 63 ++++++++++++++++++ api/tests/manage/test_serializers.py | 51 +++++++++++++++ api/tests/manage/test_views.py | 17 +++++ 14 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 api/funkwhale_api/federation/fields.py create mode 100644 api/funkwhale_api/moderation/__init__.py create mode 100644 api/funkwhale_api/moderation/factories.py create mode 100644 api/funkwhale_api/moderation/migrations/0001_initial.py create mode 100644 api/funkwhale_api/moderation/migrations/__init__.py create mode 100644 api/funkwhale_api/moderation/models.py diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 7c10a39be..74fe79ed0 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -156,6 +156,7 @@ LOCAL_APPS = ( "funkwhale_api.requests", "funkwhale_api.favorites", "funkwhale_api.federation", + "funkwhale_api.moderation", "funkwhale_api.radios", "funkwhale_api.history", "funkwhale_api.playlists", diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 331a5f5d6..f54f68678 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -67,7 +67,7 @@ def create_user(actor): @registry.register -class Domain(NoUpdateOnCreate, factory.django.DjangoModelFactory): +class DomainFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory): name = factory.Faker("domain_name") class Meta: @@ -81,7 +81,7 @@ class ActorFactory(NoUpdateOnCreate, factory.DjangoModelFactory): private_key = None preferred_username = factory.Faker("user_name") summary = factory.Faker("paragraph") - domain = factory.SubFactory(Domain) + domain = factory.SubFactory(DomainFactory) fid = factory.LazyAttribute( lambda o: "https://{}/users/{}".format(o.domain.name, o.preferred_username) ) diff --git a/api/funkwhale_api/federation/fields.py b/api/funkwhale_api/federation/fields.py new file mode 100644 index 000000000..f23d0907b --- /dev/null +++ b/api/funkwhale_api/federation/fields.py @@ -0,0 +1,18 @@ +from rest_framework import serializers + +from . import models + + +class ActorRelatedField(serializers.EmailField): + def to_representation(self, value): + return value.full_username + + def to_interal_value(self, value): + value = super().to_interal_value(value) + username, domain = value.split("@") + try: + return models.Actor.objects.get( + preferred_username=username, domain_id=domain + ) + except models.Actor.DoesNotExist: + raise serializers.ValidationError("Invalid actor name") diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 51648298a..b2088b5a6 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -4,6 +4,7 @@ from funkwhale_api.common import fields from funkwhale_api.common import search from funkwhale_api.federation import models as federation_models +from funkwhale_api.moderation import models as moderation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models @@ -87,3 +88,24 @@ class ManageInvitationFilterSet(filters.FilterSet): if value is None: return queryset return queryset.open(value) + + +class ManageInstancePolicyFilterSet(filters.FilterSet): + q = fields.SearchFilter( + search_fields=[ + "summary", + "target_domain__name", + "target_actor__username", + "target_actor__domain__name", + ] + ) + + class Meta: + model = moderation_models.InstancePolicy + fields = [ + "q", + "block_all", + "silence_activity", + "silence_notifications", + "reject_media", + ] diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 76d0cf05f..009f5c31d 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -4,6 +4,8 @@ from rest_framework import serializers from funkwhale_api.common import serializers as common_serializers from funkwhale_api.federation import models as federation_models +from funkwhale_api.federation import fields as federation_fields +from funkwhale_api.moderation import models as moderation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models @@ -185,6 +187,13 @@ class ManageDomainSerializer(serializers.ModelSerializer): "outbox_activities_count", "nodeinfo", "nodeinfo_fetch_date", + "instance_policy", + ] + read_only_fields = [ + "creation_date", + "instance_policy", + "nodeinfo", + "nodeinfo_fetch_date", ] def get_actors_count(self, o): @@ -218,7 +227,62 @@ class ManageActorSerializer(serializers.ModelSerializer): "manually_approves_followers", "uploads_count", "user", + "instance_policy", ] + read_only_fields = ["creation_date", "instance_policy"] def get_uploads_count(self, o): return getattr(o, "uploads_count", 0) + + +class TargetSerializer(serializers.Serializer): + type = serializers.ChoiceField(choices=["domain", "actor"]) + id = serializers.CharField() + + def to_representation(self, value): + if value["type"] == "domain": + return {"type": "domain", "id": value["obj"].name} + if value["type"] == "actor": + return {"type": "actor", "id": value["obj"].full_username} + + def to_internal_value(self, value): + if value["type"] == "domain": + field = serializers.PrimaryKeyRelatedField( + queryset=federation_models.Domain.objects.external() + ) + if value["type"] == "actor": + field = federation_fields.ActorRelatedField() + value["obj"] = field.to_internal_value(value["id"]) + return value + + +class ManageInstancePolicySerializer(serializers.ModelSerializer): + target = TargetSerializer() + actor = federation_fields.ActorRelatedField(read_only=True) + + class Meta: + model = moderation_models.InstancePolicy + fields = [ + "id", + "uuid", + "target", + "creation_date", + "actor", + "summary", + "is_active", + "block_all", + "silence_activity", + "silence_notifications", + "reject_media", + ] + + read_only_fields = ["uuid", "id", "creation_date", "actor", "target"] + + def validate(self, data): + target = data.pop("target") + if target["type"] == "domain": + data["target_domain"] = target["obj"] + if target["type"] == "actor": + data["target_actor"] = target["obj"] + + return data diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index 232b88711..4c220fe0e 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -5,8 +5,15 @@ from . import views federation_router = routers.SimpleRouter() federation_router.register(r"domains", views.ManageDomainViewSet, "domains") + library_router = routers.SimpleRouter() library_router.register(r"uploads", views.ManageUploadViewSet, "uploads") + +moderation_router = routers.SimpleRouter() +moderation_router.register( + r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies" +) + users_router = routers.SimpleRouter() users_router.register(r"users", views.ManageUserViewSet, "users") users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations") @@ -20,5 +27,9 @@ urlpatterns = [ include((federation_router.urls, "federation"), namespace="federation"), ), url(r"^library/", include((library_router.urls, "instance"), namespace="library")), + url( + r"^moderation/", + include((moderation_router.urls, "moderation"), namespace="moderation"), + ), url(r"^users/", include((users_router.urls, "instance"), namespace="users")), ] + other_router.urls diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 763b37497..f1fbf01a4 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -6,6 +6,7 @@ from funkwhale_api.common import preferences from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import tasks as federation_tasks from funkwhale_api.music import models as music_models +from funkwhale_api.moderation import models as moderation_models from funkwhale_api.users import models as users_models from funkwhale_api.users.permissions import HasUserPermission @@ -173,3 +174,26 @@ class ManageActorViewSet( def stats(self, request, *args, **kwargs): domain = self.get_object() return response.Response(domain.get_stats(), status=200) + + +class ManageInstancePolicyViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, +): + queryset = ( + moderation_models.InstancePolicy.objects.all() + .order_by("-creation_date") + .select_related() + ) + serializer_class = serializers.ManageInstancePolicySerializer + filter_class = filters.ManageInstancePolicyFilterSet + permission_classes = (HasUserPermission,) + required_permissions = ["moderation"] + ordering_fields = ["id", "creation_date"] + + def perform_create(self, serializer): + serializer.save(actor=self.request.user.actor) diff --git a/api/funkwhale_api/moderation/__init__.py b/api/funkwhale_api/moderation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/funkwhale_api/moderation/factories.py b/api/funkwhale_api/moderation/factories.py new file mode 100644 index 000000000..aba5256c9 --- /dev/null +++ b/api/funkwhale_api/moderation/factories.py @@ -0,0 +1,23 @@ +import factory + +from funkwhale_api.factories import registry, NoUpdateOnCreate +from funkwhale_api.federation import factories as federation_factories + + +@registry.register +class InstancePolicyFactory(NoUpdateOnCreate, factory.DjangoModelFactory): + summary = factory.Faker("paragraph") + actor = factory.SubFactory(federation_factories.ActorFactory) + block_all = True + is_active = True + + class Meta: + model = "moderation.InstancePolicy" + + class Params: + for_domain = factory.Trait( + target_domain=factory.SubFactory(federation_factories.DomainFactory) + ) + for_actor = factory.Trait( + target_actor=factory.SubFactory(federation_factories.ActorFactory) + ) diff --git a/api/funkwhale_api/moderation/migrations/0001_initial.py b/api/funkwhale_api/moderation/migrations/0001_initial.py new file mode 100644 index 000000000..33151e00f --- /dev/null +++ b/api/funkwhale_api/moderation/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 2.0.9 on 2019-01-07 06:06 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('federation', '0016_auto_20181227_1605'), + ] + + operations = [ + migrations.CreateModel( + name='InstancePolicy', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('is_active', models.BooleanField(default=True)), + ('summary', models.TextField(blank=True, max_length=10000, null=True)), + ('block_all', models.BooleanField(default=False)), + ('silence_activity', models.BooleanField(default=False)), + ('silence_notifications', models.BooleanField(default=False)), + ('reject_media', models.BooleanField(default=False)), + ('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_instance_policies', to='federation.Actor')), + ('target_actor', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='instance_policy', to='federation.Actor')), + ('target_domain', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='instance_policy', to='federation.Domain')), + ], + ), + ] diff --git a/api/funkwhale_api/moderation/migrations/__init__.py b/api/funkwhale_api/moderation/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/funkwhale_api/moderation/models.py b/api/funkwhale_api/moderation/models.py new file mode 100644 index 000000000..59b077bab --- /dev/null +++ b/api/funkwhale_api/moderation/models.py @@ -0,0 +1,63 @@ +import urllib.parse +import uuid + +from django.db import models +from django.utils import timezone + + +class InstancePolicyQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + def matching_url(self, url): + parsed = urllib.parse.urlparse(url) + return self.filter( + models.Q(target_domain_id=parsed.hostname) | models.Q(target_actor__fid=url) + ) + + +class InstancePolicy(models.Model): + uuid = models.UUIDField(default=uuid.uuid4, unique=True) + actor = models.ForeignKey( + "federation.Actor", + related_name="created_instance_policies", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + target_domain = models.OneToOneField( + "federation.Domain", + related_name="instance_policy", + on_delete=models.CASCADE, + null=True, + blank=True, + ) + target_actor = models.OneToOneField( + "federation.Actor", + related_name="instance_policy", + on_delete=models.CASCADE, + null=True, + blank=True, + ) + creation_date = models.DateTimeField(default=timezone.now) + + is_active = models.BooleanField(default=True) + # a summary explaining why the policy is in place + summary = models.TextField(max_length=10000, null=True, blank=True) + # either block everything (simpler, but less granularity) + block_all = models.BooleanField(default=False) + # or pick individual restrictions below + # do not show in timelines/notifications, except for actual followers + silence_activity = models.BooleanField(default=False) + silence_notifications = models.BooleanField(default=False) + # do not download any media from the target + reject_media = models.BooleanField(default=False) + + objects = InstancePolicyQuerySet.as_manager() + + @property + def target(self): + if self.target_actor: + return {"type": "actor", "obj": self.target_actor} + if self.target_domain_id: + return {"type": "domain", "obj": self.target_domain} diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 74ba96ba8..6dbd7ac3a 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -49,6 +49,7 @@ def test_manage_domain_serializer(factories, now): "outbox_activities_count": 23, "nodeinfo": {}, "nodeinfo_fetch_date": None, + "instance_policy": None, } s = serializers.ManageDomainSerializer(domain) @@ -83,7 +84,57 @@ def test_manage_actor_serializer(factories, now): "manually_approves_followers": actor.manually_approves_followers, "full_username": actor.full_username, "user": None, + "instance_policy": None, } s = serializers.ManageActorSerializer(actor) assert s.data == expected + + +@pytest.mark.parametrize( + "factory_kwargs,expected", + [ + ( + {"for_domain": True, "target_domain__name": "test.federation"}, + {"target": {"type": "domain", "id": "test.federation"}}, + ), + ( + { + "for_actor": True, + "target_actor__domain__name": "test.federation", + "target_actor__preferred_username": "hello", + }, + {"target": {"type": "actor", "id": "hello@test.federation"}}, + ), + ], +) +def test_instance_policy_serializer_repr(factories, factory_kwargs, expected): + policy = factories["moderation.InstancePolicy"](block_all=True, **factory_kwargs) + + e = { + "id": policy.id, + "uuid": str(policy.uuid), + "creation_date": policy.creation_date.isoformat().split("+")[0] + "Z", + "actor": policy.actor.full_username, + "block_all": True, + "silence_activity": False, + "silence_notifications": False, + "reject_media": False, + "is_active": policy.is_active, + "summary": policy.summary, + } + e.update(expected) + + assert serializers.ManageInstancePolicySerializer(policy).data == e + + +def test_instance_policy_serializer_save_domain(factories): + domain = factories["federation.Domain"]() + + data = {"target": {"id": domain.name, "type": "domain"}, "block_all": True} + + serializer = serializers.ManageInstancePolicySerializer(data=data) + serializer.is_valid(raise_exception=True) + policy = serializer.save() + + assert policy.target_domain == domain diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index 4591f7b1b..6402fb650 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -14,6 +14,7 @@ from funkwhale_api.manage import serializers, views (views.ManageInvitationViewSet, ["settings"], "and"), (views.ManageDomainViewSet, ["moderation"], "and"), (views.ManageActorViewSet, ["moderation"], "and"), + (views.ManageInstancePolicyViewSet, ["moderation"], "and"), ], ) def test_permissions(assert_user_permission, view, permissions, operator): @@ -142,3 +143,19 @@ def test_actor_detail(factories, superuser_api_client): assert response.status_code == 200 assert response.data["id"] == actor.id + + +def test_instance_policy_create(superuser_api_client, factories): + domain = factories["federation.Domain"]() + actor = superuser_api_client.user.create_actor() + url = reverse("api:v1:manage:moderation:instance-policies-list") + response = superuser_api_client.post( + url, + {"target": {"type": "domain", "id": domain.name}, "block_all": True}, + format="json", + ) + + assert response.status_code == 201 + + policy = domain.instance_policy + assert policy.actor == actor From 833daa242c0b0c3e12c5a0286183b9292fe29d96 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 7 Jan 2019 09:46:19 +0100 Subject: [PATCH 03/15] UI for managing domain policies --- front/src/components/common/Tooltip.vue | 12 + front/src/components/globals.js | 4 + .../manage/moderation/InstancePolicyCard.vue | 72 ++++++ .../manage/moderation/InstancePolicyForm.vue | 212 ++++++++++++++++++ front/src/style/_main.scss | 8 +- .../views/admin/moderation/DomainsDetail.vue | 95 +++++++- 6 files changed, 389 insertions(+), 14 deletions(-) create mode 100644 front/src/components/common/Tooltip.vue create mode 100644 front/src/components/manage/moderation/InstancePolicyCard.vue create mode 100644 front/src/components/manage/moderation/InstancePolicyForm.vue diff --git a/front/src/components/common/Tooltip.vue b/front/src/components/common/Tooltip.vue new file mode 100644 index 000000000..d9ba4c13c --- /dev/null +++ b/front/src/components/common/Tooltip.vue @@ -0,0 +1,12 @@ + + + diff --git a/front/src/components/globals.js b/front/src/components/globals.js index d5a1fb4a4..99e57095c 100644 --- a/front/src/components/globals.js +++ b/front/src/components/globals.js @@ -40,5 +40,9 @@ import AjaxButton from '@/components/common/AjaxButton' Vue.component('ajax-button', AjaxButton) +import Tooltip from '@/components/common/Tooltip' + +Vue.component('tooltip', Tooltip) + export default {} diff --git a/front/src/components/manage/moderation/InstancePolicyCard.vue b/front/src/components/manage/moderation/InstancePolicyCard.vue new file mode 100644 index 000000000..c7d115856 --- /dev/null +++ b/front/src/components/manage/moderation/InstancePolicyCard.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/front/src/components/manage/moderation/InstancePolicyForm.vue b/front/src/components/manage/moderation/InstancePolicyForm.vue new file mode 100644 index 000000000..d3c8d6d6e --- /dev/null +++ b/front/src/components/manage/moderation/InstancePolicyForm.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index 1ce8144c6..0c165c76f 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -255,7 +255,11 @@ button.reset { [data-tooltip]::after { white-space: normal; - width: 300px; - max-width: 300px; + width: 500px; + max-width: 500px; z-index: 999; } + +label .tooltip { + margin-left: 1em; +} diff --git a/front/src/views/admin/moderation/DomainsDetail.vue b/front/src/views/admin/moderation/DomainsDetail.vue index 1adb1c305..f5f9643c8 100644 --- a/front/src/views/admin/moderation/DomainsDetail.vue +++ b/front/src/views/admin/moderation/DomainsDetail.vue @@ -5,19 +5,61 @@