See #890: added moderation note model, serializers and views
This commit is contained in:
parent
80c8610632
commit
ab3bc96783
|
@ -363,3 +363,19 @@ class ManageReportFilterSet(filters.FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = moderation_models.Report
|
model = moderation_models.Report
|
||||||
fields = ["q", "is_handled", "type", "submitter_email"]
|
fields = ["q", "is_handled", "type", "submitter_email"]
|
||||||
|
|
||||||
|
|
||||||
|
class ManageNoteFilterSet(filters.FilterSet):
|
||||||
|
q = fields.SmartSearchFilter(
|
||||||
|
config=search.SearchConfig(
|
||||||
|
search_fields={"summary": {"to": "summary"}},
|
||||||
|
filter_fields={
|
||||||
|
"uuid": {"to": "uuid"},
|
||||||
|
"author": get_actor_filter("author"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = moderation_models.Note
|
||||||
|
fields = ["q"]
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.db import transaction
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from funkwhale_api.common import fields as common_fields
|
||||||
from funkwhale_api.common import serializers as common_serializers
|
from funkwhale_api.common import serializers as common_serializers
|
||||||
from funkwhale_api.common import utils as common_utils
|
from funkwhale_api.common import utils as common_utils
|
||||||
from funkwhale_api.federation import models as federation_models
|
from funkwhale_api.federation import models as federation_models
|
||||||
|
@ -676,3 +677,27 @@ class ManageReportSerializer(serializers.ModelSerializer):
|
||||||
"target_owner",
|
"target_owner",
|
||||||
"summary",
|
"summary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ManageNoteSerializer(serializers.ModelSerializer):
|
||||||
|
author = ManageBaseActorSerializer(required=False)
|
||||||
|
target = common_fields.GenericRelation(
|
||||||
|
{
|
||||||
|
"report": {
|
||||||
|
"queryset": moderation_models.Report.objects.all(),
|
||||||
|
"id_attr": "uuid",
|
||||||
|
"id_field": serializers.UUIDField(),
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"queryset": federation_models.Actor.objects.all(),
|
||||||
|
"id_attr": "full_username",
|
||||||
|
"id_field": serializers.EmailField(),
|
||||||
|
"get_query": moderation_serializers.get_actor_query,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = moderation_models.Note
|
||||||
|
fields = ["id", "uuid", "creation_date", "summary", "author", "target"]
|
||||||
|
read_only_fields = ["uuid", "creation_date", "author"]
|
||||||
|
|
|
@ -18,6 +18,7 @@ moderation_router.register(
|
||||||
r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies"
|
r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies"
|
||||||
)
|
)
|
||||||
moderation_router.register(r"reports", views.ManageReportViewSet, "reports")
|
moderation_router.register(r"reports", views.ManageReportViewSet, "reports")
|
||||||
|
moderation_router.register(r"notes", views.ManageNoteViewSet, "notes")
|
||||||
|
|
||||||
users_router = routers.OptionalSlashRouter()
|
users_router = routers.OptionalSlashRouter()
|
||||||
users_router.register(r"users", views.ManageUserViewSet, "users")
|
users_router.register(r"users", views.ManageUserViewSet, "users")
|
||||||
|
|
|
@ -478,6 +478,27 @@ class ManageReportViewSet(
|
||||||
ordering_fields = ["id", "creation_date", "handled_date"]
|
ordering_fields = ["id", "creation_date", "handled_date"]
|
||||||
|
|
||||||
|
|
||||||
|
class ManageNoteViewSet(
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
lookup_field = "uuid"
|
||||||
|
queryset = (
|
||||||
|
moderation_models.Note.objects.all().order_by("-creation_date").select_related()
|
||||||
|
)
|
||||||
|
serializer_class = serializers.ManageNoteSerializer
|
||||||
|
filterset_class = filters.ManageNoteFilterSet
|
||||||
|
required_scope = "instance:notes"
|
||||||
|
ordering_fields = ["id", "creation_date"]
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
author = self.request.user.actor
|
||||||
|
return serializer.save(author=author)
|
||||||
|
|
||||||
|
|
||||||
class ManageTagViewSet(
|
class ManageTagViewSet(
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
|
|
|
@ -30,7 +30,6 @@ class InstancePolicyAdmin(admin.ModelAdmin):
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Report)
|
@admin.register(models.Report)
|
||||||
class ReportAdmin(admin.ModelAdmin):
|
class ReportAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
|
@ -42,13 +41,8 @@ class ReportAdmin(admin.ModelAdmin):
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"handled_date",
|
"handled_date",
|
||||||
]
|
]
|
||||||
list_filter = [
|
list_filter = ["type", "is_handled"]
|
||||||
"type",
|
search_fields = ["summary"]
|
||||||
"is_handled",
|
|
||||||
]
|
|
||||||
search_fields = [
|
|
||||||
"summary",
|
|
||||||
]
|
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,16 @@ class UserFilterFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
class NoteFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||||
|
author = factory.SubFactory(federation_factories.ActorFactory)
|
||||||
|
target = None
|
||||||
|
summary = factory.Faker("paragraph")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = "moderation.Note"
|
||||||
|
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
class ReportFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||||
submitter = factory.SubFactory(federation_factories.ActorFactory)
|
submitter = factory.SubFactory(federation_factories.ActorFactory)
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-08-29 09:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('federation', '0020_auto_20190730_0846'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('moderation', '0003_report'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Note',
|
||||||
|
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)),
|
||||||
|
('summary', models.TextField(max_length=50000)),
|
||||||
|
('target_id', models.IntegerField(null=True)),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_notes', to='federation.Actor')),
|
||||||
|
('target_content_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -164,6 +164,21 @@ class Report(federation_models.FederationMixin):
|
||||||
return super().save(**kwargs)
|
return super().save(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Note(models.Model):
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
|
summary = models.TextField(max_length=50000)
|
||||||
|
author = models.ForeignKey(
|
||||||
|
"federation.Actor", related_name="moderation_notes", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
target_id = models.IntegerField(null=True)
|
||||||
|
target_content_type = models.ForeignKey(
|
||||||
|
ContentType, null=True, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
target = GenericForeignKey("target_content_type", "target_id")
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Report)
|
@receiver(pre_save, sender=Report)
|
||||||
def set_handled_date(sender, instance, **kwargs):
|
def set_handled_date(sender, instance, **kwargs):
|
||||||
if instance.is_handled is True and not instance.handled_date:
|
if instance.is_handled is True and not instance.handled_date:
|
||||||
|
|
|
@ -119,7 +119,15 @@ class TrackStateSerializer(serializers.ModelSerializer):
|
||||||
class LibraryStateSerializer(serializers.ModelSerializer):
|
class LibraryStateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Library
|
model = music_models.Library
|
||||||
fields = ["id", "uuid", "fid", "name", "description", "creation_date", "privacy_level"]
|
fields = [
|
||||||
|
"id",
|
||||||
|
"uuid",
|
||||||
|
"fid",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"creation_date",
|
||||||
|
"privacy_level",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@state_serializers.register(name="playlists.Playlist")
|
@state_serializers.register(name="playlists.Playlist")
|
||||||
|
|
|
@ -48,6 +48,8 @@ PERMISSIONS_CONFIGURATION = {
|
||||||
"write:instance:domains",
|
"write:instance:domains",
|
||||||
"read:instance:reports",
|
"read:instance:reports",
|
||||||
"write:instance:reports",
|
"write:instance:reports",
|
||||||
|
"read:instance:notes",
|
||||||
|
"write:instance:notes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
|
|
|
@ -35,6 +35,7 @@ BASE_SCOPES = [
|
||||||
Scope("instance:domains", "Access instance domains"),
|
Scope("instance:domains", "Access instance domains"),
|
||||||
Scope("instance:policies", "Access instance moderation policies"),
|
Scope("instance:policies", "Access instance moderation policies"),
|
||||||
Scope("instance:reports", "Access instance moderation reports"),
|
Scope("instance:reports", "Access instance moderation reports"),
|
||||||
|
Scope("instance:notes", "Access instance moderation notes"),
|
||||||
]
|
]
|
||||||
SCOPES = [
|
SCOPES = [
|
||||||
Scope("read", children=[s.copy("read") for s in BASE_SCOPES]),
|
Scope("read", children=[s.copy("read") for s in BASE_SCOPES]),
|
||||||
|
|
|
@ -550,3 +550,20 @@ def test_manage_report_serializer(factories, to_api_date):
|
||||||
s = serializers.ManageReportSerializer(report)
|
s = serializers.ManageReportSerializer(report)
|
||||||
|
|
||||||
assert s.data == expected
|
assert s.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_manage_note_serializer(factories, to_api_date):
|
||||||
|
actor = factories["federation.Actor"]()
|
||||||
|
note = factories["moderation.Note"](target=actor)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"id": note.id,
|
||||||
|
"uuid": str(note.uuid),
|
||||||
|
"summary": note.summary,
|
||||||
|
"creation_date": to_api_date(note.creation_date),
|
||||||
|
"author": serializers.ManageBaseActorSerializer(note.author).data,
|
||||||
|
"target": {"type": "account", "full_username": actor.full_username},
|
||||||
|
}
|
||||||
|
s = serializers.ManageNoteSerializer(note)
|
||||||
|
|
||||||
|
assert s.data == expected
|
||||||
|
|
|
@ -391,6 +391,50 @@ def test_upload_delete(factories, superuser_api_client):
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_note_create(factories, superuser_api_client):
|
||||||
|
actor = superuser_api_client.user.create_actor()
|
||||||
|
target = factories["federation.Actor"]()
|
||||||
|
data = {
|
||||||
|
"summary": "Hello",
|
||||||
|
"target": {"type": "account", "full_username": target.full_username},
|
||||||
|
}
|
||||||
|
url = reverse("api:v1:manage:moderation:notes-list")
|
||||||
|
response = superuser_api_client.post(url, data, format="json")
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
note = actor.moderation_notes.latest("id")
|
||||||
|
assert note.target == target
|
||||||
|
assert response.data == serializers.ManageNoteSerializer(note).data
|
||||||
|
|
||||||
|
|
||||||
|
def test_note_list(factories, superuser_api_client, settings):
|
||||||
|
note = factories["moderation.Note"]()
|
||||||
|
url = reverse("api:v1:manage:moderation:notes-list")
|
||||||
|
response = superuser_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
assert response.data["count"] == 1
|
||||||
|
assert response.data["results"][0] == serializers.ManageNoteSerializer(note).data
|
||||||
|
|
||||||
|
|
||||||
|
def test_note_delete(factories, superuser_api_client):
|
||||||
|
note = factories["moderation.Note"]()
|
||||||
|
url = reverse("api:v1:manage:moderation:notes-detail", kwargs={"uuid": note.uuid})
|
||||||
|
response = superuser_api_client.delete(url)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_note_detail(factories, superuser_api_client):
|
||||||
|
note = factories["moderation.Note"]()
|
||||||
|
url = reverse("api:v1:manage:moderation:notes-detail", kwargs={"uuid": note.uuid})
|
||||||
|
response = superuser_api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == serializers.ManageNoteSerializer(note).data
|
||||||
|
|
||||||
|
|
||||||
def test_tag_detail(factories, superuser_api_client):
|
def test_tag_detail(factories, superuser_api_client):
|
||||||
tag = factories["tags.Tag"]()
|
tag = factories["tags.Tag"]()
|
||||||
url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name})
|
url = reverse("api:v1:manage:tags-detail", kwargs={"name": tag.name})
|
||||||
|
|
Loading…
Reference in New Issue