Report UI (end-user)
This commit is contained in:
parent
1a8edf27b3
commit
33d1f879cf
|
@ -1,6 +1,7 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.activity import serializers as activity_serializers
|
||||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer
|
||||
from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer
|
||||
|
||||
|
@ -27,10 +28,17 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
|
|||
class UserTrackFavoriteSerializer(serializers.ModelSerializer):
|
||||
track = TrackSerializer(read_only=True)
|
||||
user = UserBasicSerializer(read_only=True)
|
||||
actor = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.TrackFavorite
|
||||
fields = ("id", "user", "track", "creation_date")
|
||||
fields = ("id", "user", "track", "creation_date", "actor")
|
||||
actor = serializers.SerializerMethodField()
|
||||
|
||||
def get_actor(self, obj):
|
||||
actor = obj.user.actor
|
||||
if actor:
|
||||
return federation_serializers.APIActorSerializer(actor).data
|
||||
|
||||
|
||||
class UserTrackFavoriteWriteSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -22,7 +22,7 @@ class TrackFavoriteViewSet(
|
|||
|
||||
filterset_class = filters.TrackFavoriteFilter
|
||||
serializer_class = serializers.UserTrackFavoriteSerializer
|
||||
queryset = models.TrackFavorite.objects.all().select_related("user")
|
||||
queryset = models.TrackFavorite.objects.all().select_related("user__actor")
|
||||
permission_classes = [
|
||||
oauth_permissions.ScopePermission,
|
||||
permissions.OwnerPermission,
|
||||
|
@ -54,7 +54,7 @@ class TrackFavoriteViewSet(
|
|||
)
|
||||
tracks = Track.objects.with_playable_uploads(
|
||||
music_utils.get_actor_from_request(self.request)
|
||||
).select_related("artist", "album__artist")
|
||||
).select_related("artist", "album__artist", "attributed_to")
|
||||
queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
||||
return queryset
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.activity import serializers as activity_serializers
|
||||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
from funkwhale_api.music.serializers import TrackActivitySerializer, TrackSerializer
|
||||
from funkwhale_api.users.serializers import UserActivitySerializer, UserBasicSerializer
|
||||
|
||||
|
@ -27,16 +28,22 @@ class ListeningActivitySerializer(activity_serializers.ModelSerializer):
|
|||
class ListeningSerializer(serializers.ModelSerializer):
|
||||
track = TrackSerializer(read_only=True)
|
||||
user = UserBasicSerializer(read_only=True)
|
||||
actor = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.Listening
|
||||
fields = ("id", "user", "track", "creation_date")
|
||||
fields = ("id", "user", "track", "creation_date", "actor")
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data["user"] = self.context["user"]
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
def get_actor(self, obj):
|
||||
actor = obj.user.actor
|
||||
if actor:
|
||||
return federation_serializers.APIActorSerializer(actor).data
|
||||
|
||||
|
||||
class ListeningWriteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -19,7 +19,7 @@ class ListeningViewSet(
|
|||
):
|
||||
|
||||
serializer_class = serializers.ListeningSerializer
|
||||
queryset = models.Listening.objects.all().select_related("user")
|
||||
queryset = models.Listening.objects.all().select_related("user__actor")
|
||||
|
||||
permission_classes = [
|
||||
oauth_permissions.ScopePermission,
|
||||
|
@ -47,7 +47,7 @@ class ListeningViewSet(
|
|||
)
|
||||
tracks = Track.objects.with_playable_uploads(
|
||||
music_utils.get_actor_from_request(self.request)
|
||||
).select_related("artist", "album__artist")
|
||||
).select_related("artist", "album__artist", "attributed_to")
|
||||
return queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
|
|
@ -3,6 +3,7 @@ import memoize.djangocache
|
|||
import funkwhale_api
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.federation import actors, models as federation_models
|
||||
from funkwhale_api.moderation import models as moderation_models
|
||||
from funkwhale_api.music import utils as music_utils
|
||||
|
||||
from . import stats
|
||||
|
@ -15,6 +16,9 @@ def get():
|
|||
share_stats = preferences.get("instance__nodeinfo_stats_enabled")
|
||||
allow_list_enabled = preferences.get("moderation__allow_list_enabled")
|
||||
allow_list_public = preferences.get("moderation__allow_list_public")
|
||||
unauthenticated_report_types = preferences.get(
|
||||
"moderation__unauthenticated_report_types"
|
||||
)
|
||||
if allow_list_enabled and allow_list_public:
|
||||
allowed_domains = list(
|
||||
federation_models.Domain.objects.filter(allowed=True)
|
||||
|
@ -47,6 +51,10 @@ def get():
|
|||
},
|
||||
"supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS,
|
||||
"allowList": {"enabled": allow_list_enabled, "domains": allowed_domains},
|
||||
"reportTypes": [
|
||||
{"type": t, "label": l, "anonymous": t in unauthenticated_report_types}
|
||||
for t, l in moderation_models.REPORT_TYPES
|
||||
],
|
||||
},
|
||||
}
|
||||
if share_stats:
|
||||
|
|
|
@ -115,7 +115,7 @@ REPORT_TYPES = [
|
|||
class Report(federation_models.FederationMixin):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
summary = models.TextField(null=True, max_length=50000)
|
||||
summary = models.TextField(null=True, blank=True, max_length=50000)
|
||||
handled_date = models.DateTimeField(null=True)
|
||||
is_handled = models.BooleanField(default=False)
|
||||
type = models.CharField(max_length=40, choices=REPORT_TYPES)
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.db import transaction
|
|||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
from funkwhale_api.music.models import Track
|
||||
from funkwhale_api.music.serializers import TrackSerializer
|
||||
from funkwhale_api.users.serializers import UserBasicSerializer
|
||||
|
@ -79,6 +80,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
|||
album_covers = serializers.SerializerMethodField(read_only=True)
|
||||
user = UserBasicSerializer(read_only=True)
|
||||
is_playable = serializers.SerializerMethodField()
|
||||
actor = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.Playlist
|
||||
|
@ -93,9 +95,15 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
|||
"album_covers",
|
||||
"duration",
|
||||
"is_playable",
|
||||
"actor",
|
||||
)
|
||||
read_only_fields = ["id", "modification_date", "creation_date"]
|
||||
|
||||
def get_actor(self, obj):
|
||||
actor = obj.user.actor
|
||||
if actor:
|
||||
return federation_serializers.APIActorSerializer(actor).data
|
||||
|
||||
def get_is_playable(self, obj):
|
||||
try:
|
||||
return bool(obj.playable_plts)
|
||||
|
|
|
@ -23,7 +23,7 @@ class PlaylistViewSet(
|
|||
serializer_class = serializers.PlaylistSerializer
|
||||
queryset = (
|
||||
models.Playlist.objects.all()
|
||||
.select_related("user")
|
||||
.select_related("user__actor")
|
||||
.annotate(tracks_count=Count("playlist_tracks"))
|
||||
.with_covers()
|
||||
.with_duration()
|
||||
|
|
|
@ -4,8 +4,7 @@ import pytest
|
|||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.favorites.models import TrackFavorite
|
||||
from funkwhale_api.music import serializers as music_serializers
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
from funkwhale_api.favorites import serializers
|
||||
|
||||
|
||||
def test_user_can_add_favorite(factories):
|
||||
|
@ -20,22 +19,15 @@ def test_user_can_add_favorite(factories):
|
|||
def test_user_can_get_his_favorites(
|
||||
api_request, factories, logged_in_api_client, client
|
||||
):
|
||||
r = api_request.get("/")
|
||||
request = api_request.get("/")
|
||||
favorite = factories["favorites.TrackFavorite"](user=logged_in_api_client.user)
|
||||
factories["favorites.TrackFavorite"]()
|
||||
url = reverse("api:v1:favorites:tracks-list")
|
||||
response = logged_in_api_client.get(url, {"user": logged_in_api_client.user.pk})
|
||||
expected = [
|
||||
{
|
||||
"user": users_serializers.UserBasicSerializer(
|
||||
favorite.user, context={"request": r}
|
||||
).data,
|
||||
"track": music_serializers.TrackSerializer(
|
||||
favorite.track, context={"request": r}
|
||||
).data,
|
||||
"id": favorite.id,
|
||||
"creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
serializers.UserTrackFavoriteSerializer(
|
||||
favorite, context={"request": request}
|
||||
).data
|
||||
]
|
||||
assert response.status_code == 200
|
||||
assert response.data["results"] == expected
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
from funkwhale_api.favorites import serializers
|
||||
from funkwhale_api.music import serializers as music_serializers
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
|
||||
|
||||
def test_track_favorite_serializer(factories, to_api_date):
|
||||
favorite = factories["favorites.TrackFavorite"]()
|
||||
actor = favorite.user.create_actor()
|
||||
|
||||
expected = {
|
||||
"id": favorite.pk,
|
||||
"creation_date": to_api_date(favorite.creation_date),
|
||||
"track": music_serializers.TrackSerializer(favorite.track).data,
|
||||
"actor": federation_serializers.APIActorSerializer(actor).data,
|
||||
"user": users_serializers.UserBasicSerializer(favorite.user).data,
|
||||
}
|
||||
serializer = serializers.UserTrackFavoriteSerializer(favorite)
|
||||
|
||||
assert serializer.data == expected
|
|
@ -0,0 +1,20 @@
|
|||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
from funkwhale_api.history import serializers
|
||||
from funkwhale_api.music import serializers as music_serializers
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
|
||||
|
||||
def test_listening_serializer(factories, to_api_date):
|
||||
listening = factories["history.Listening"]()
|
||||
actor = listening.user.create_actor()
|
||||
|
||||
expected = {
|
||||
"id": listening.pk,
|
||||
"creation_date": to_api_date(listening.creation_date),
|
||||
"track": music_serializers.TrackSerializer(listening.track).data,
|
||||
"actor": federation_serializers.APIActorSerializer(actor).data,
|
||||
"user": users_serializers.UserBasicSerializer(listening.user).data,
|
||||
}
|
||||
serializer = serializers.ListeningSerializer(listening)
|
||||
|
||||
assert serializer.data == expected
|
|
@ -8,6 +8,12 @@ from funkwhale_api.music import utils as music_utils
|
|||
|
||||
def test_nodeinfo_dump(preferences, mocker):
|
||||
preferences["instance__nodeinfo_stats_enabled"] = True
|
||||
preferences["moderation__unauthenticated_report_types"] = [
|
||||
"takedown_request",
|
||||
"other",
|
||||
"other_category_that_doesnt_exist",
|
||||
]
|
||||
|
||||
stats = {
|
||||
"users": {"total": 1, "active_halfyear": 12, "active_month": 13},
|
||||
"tracks": 2,
|
||||
|
@ -51,6 +57,29 @@ def test_nodeinfo_dump(preferences, mocker):
|
|||
},
|
||||
"supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS,
|
||||
"allowList": {"enabled": False, "domains": None},
|
||||
"reportTypes": [
|
||||
{
|
||||
"type": "takedown_request",
|
||||
"label": "Takedown request",
|
||||
"anonymous": True,
|
||||
},
|
||||
{
|
||||
"type": "invalid_metadata",
|
||||
"label": "Invalid metadata",
|
||||
"anonymous": False,
|
||||
},
|
||||
{
|
||||
"type": "illegal_content",
|
||||
"label": "Illegal content",
|
||||
"anonymous": False,
|
||||
},
|
||||
{
|
||||
"type": "offensive_content",
|
||||
"label": "Offensive content",
|
||||
"anonymous": False,
|
||||
},
|
||||
{"type": "other", "label": "Other", "anonymous": True},
|
||||
],
|
||||
},
|
||||
}
|
||||
assert nodeinfo.get() == expected
|
||||
|
@ -58,6 +87,10 @@ def test_nodeinfo_dump(preferences, mocker):
|
|||
|
||||
def test_nodeinfo_dump_stats_disabled(preferences, mocker):
|
||||
preferences["instance__nodeinfo_stats_enabled"] = False
|
||||
preferences["moderation__unauthenticated_report_types"] = [
|
||||
"takedown_request",
|
||||
"other",
|
||||
]
|
||||
|
||||
expected = {
|
||||
"version": "2.0",
|
||||
|
@ -83,6 +116,29 @@ def test_nodeinfo_dump_stats_disabled(preferences, mocker):
|
|||
},
|
||||
"supportedUploadExtensions": music_utils.SUPPORTED_EXTENSIONS,
|
||||
"allowList": {"enabled": False, "domains": None},
|
||||
"reportTypes": [
|
||||
{
|
||||
"type": "takedown_request",
|
||||
"label": "Takedown request",
|
||||
"anonymous": True,
|
||||
},
|
||||
{
|
||||
"type": "invalid_metadata",
|
||||
"label": "Invalid metadata",
|
||||
"anonymous": False,
|
||||
},
|
||||
{
|
||||
"type": "illegal_content",
|
||||
"label": "Illegal content",
|
||||
"anonymous": False,
|
||||
},
|
||||
{
|
||||
"type": "offensive_content",
|
||||
"label": "Offensive content",
|
||||
"anonymous": False,
|
||||
},
|
||||
{"type": "other", "label": "Other", "anonymous": True},
|
||||
],
|
||||
},
|
||||
}
|
||||
assert nodeinfo.get() == expected
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from funkwhale_api.federation import serializers as federation_serializers
|
||||
from funkwhale_api.playlists import models, serializers
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
|
||||
|
||||
def test_cannot_max_500_tracks_per_playlist(factories, preferences):
|
||||
|
@ -124,3 +126,25 @@ def test_playlist_serializer_include_duration(factories, api_request):
|
|||
|
||||
serializer = serializers.PlaylistSerializer(qs.get())
|
||||
assert serializer.data["duration"] == 45
|
||||
|
||||
|
||||
def test_playlist_serializer(factories, to_api_date):
|
||||
playlist = factories["playlists.Playlist"]()
|
||||
actor = playlist.user.create_actor()
|
||||
|
||||
expected = {
|
||||
"id": playlist.pk,
|
||||
"name": playlist.name,
|
||||
"privacy_level": playlist.privacy_level,
|
||||
"is_playable": None,
|
||||
"creation_date": to_api_date(playlist.creation_date),
|
||||
"modification_date": to_api_date(playlist.modification_date),
|
||||
"actor": federation_serializers.APIActorSerializer(actor).data,
|
||||
"user": users_serializers.UserBasicSerializer(playlist.user).data,
|
||||
"duration": 0,
|
||||
"tracks_count": 0,
|
||||
"album_covers": [],
|
||||
}
|
||||
serializer = serializers.PlaylistSerializer(playlist)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
></app-footer>
|
||||
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
|
||||
<filter-modal v-if="$store.state.auth.authenticated"></filter-modal>
|
||||
<report-modal></report-modal>
|
||||
<shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
|
||||
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/>
|
||||
</template>
|
||||
|
@ -41,6 +42,7 @@ import moment from 'moment'
|
|||
import locales from './locales'
|
||||
import PlaylistModal from '@/components/playlists/PlaylistModal'
|
||||
import FilterModal from '@/components/moderation/FilterModal'
|
||||
import ReportModal from '@/components/moderation/ReportModal'
|
||||
import ShortcutsModal from '@/components/ShortcutsModal'
|
||||
import SetInstanceModal from '@/components/SetInstanceModal'
|
||||
|
||||
|
@ -50,6 +52,7 @@ export default {
|
|||
Sidebar,
|
||||
AppFooter,
|
||||
FilterModal,
|
||||
ReportModal,
|
||||
PlaylistModal,
|
||||
ShortcutsModal,
|
||||
GlobalEvents,
|
||||
|
|
|
@ -27,9 +27,17 @@
|
|||
<button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio">
|
||||
<i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Start radio</translate>
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<button v-if="filterableArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist">
|
||||
<i class="eye slash outline icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate>
|
||||
</button>
|
||||
<button
|
||||
v-for="obj in getReportableObjs({track, album, artist, playlist, account})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
class="item basic"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -39,11 +47,15 @@
|
|||
import axios from 'axios'
|
||||
import jQuery from 'jquery'
|
||||
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
||||
export default {
|
||||
mixins: [ReportMixin],
|
||||
props: {
|
||||
// we can either have a single or multiple tracks to play when clicked
|
||||
tracks: {type: Array, required: false},
|
||||
track: {type: Object, required: false},
|
||||
account: {type: Object, required: false},
|
||||
dropdownIconClasses: {type: Array, required: false, default: () => { return ['dropdown'] }},
|
||||
playIconClass: {type: String, required: false, default: 'play icon'},
|
||||
buttonClasses: {type: Array, required: false, default: () => { return ['button'] }},
|
||||
|
@ -79,7 +91,8 @@ export default {
|
|||
addToQueue: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Add to current queue'),
|
||||
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
||||
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
|
||||
replacePlay: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Replace current queue')
|
||||
replacePlay: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Replace current queue'),
|
||||
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
|
||||
}
|
||||
},
|
||||
title () {
|
||||
|
@ -118,7 +131,7 @@ export default {
|
|||
if (this.artist) {
|
||||
return this.artist
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
|
|
|
@ -37,7 +37,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="one wide stretched column">
|
||||
<play-button class="basic icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']" :track="object.track"></play-button>
|
||||
<play-button
|
||||
class="basic icon"
|
||||
:account="object.actor"
|
||||
:dropdown-only="true"
|
||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']"
|
||||
:track="object.track"></play-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -74,6 +74,15 @@
|
|||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||
</router-link>
|
||||
<div class="divider"></div>
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
v-for="obj in getReportableObjs({album: object})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}">
|
||||
<i class="wrench icon"></i>
|
||||
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
||||
|
@ -105,6 +114,7 @@ import PlayButton from "@/components/audio/PlayButton"
|
|||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import TagsList from "@/components/tags/List"
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
||||
const FETCH_URL = "albums/"
|
||||
|
||||
|
@ -121,6 +131,7 @@ function groupByDisc(acc, track) {
|
|||
}
|
||||
|
||||
export default {
|
||||
mixins: [ReportMixin],
|
||||
props: ["id"],
|
||||
components: {
|
||||
PlayButton,
|
||||
|
|
|
@ -84,6 +84,16 @@
|
|||
<i class="edit icon"></i>
|
||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||
</router-link>
|
||||
<div class="divider"></div>
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
v-for="obj in getReportableObjs({artist: object})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}">
|
||||
<i class="wrench icon"></i>
|
||||
|
@ -125,12 +135,12 @@ import EmbedWizard from "@/components/audio/EmbedWizard"
|
|||
import Modal from '@/components/semantic/Modal'
|
||||
import RadioButton from "@/components/radios/Button"
|
||||
import TagsList from "@/components/tags/List"
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
||||
const FETCH_URL = "albums/"
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
mixins: [ReportMixin],
|
||||
props: ["id"],
|
||||
components: {
|
||||
PlayButton,
|
||||
|
|
|
@ -90,6 +90,15 @@
|
|||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||
</router-link>
|
||||
<div class="divider"></div>
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
v-for="obj in getReportableObjs({track})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.tracks.detail', params: {id: track.id}}">
|
||||
<i class="wrench icon"></i>
|
||||
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
||||
|
@ -124,11 +133,13 @@ import TrackPlaylistIcon from "@/components/playlists/TrackPlaylistIcon"
|
|||
import Modal from '@/components/semantic/Modal'
|
||||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
||||
import TagsList from "@/components/tags/List"
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
|
||||
const FETCH_URL = "tracks/"
|
||||
|
||||
export default {
|
||||
props: ["id"],
|
||||
mixins: [ReportMixin],
|
||||
components: {
|
||||
PlayButton,
|
||||
TrackPlaylistIcon,
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
getReportableObjs ({track, album, artist, playlist, account}) {
|
||||
let reportableObjs = []
|
||||
if (account) {
|
||||
let accountLabel = this.$pgettext('*/Moderation/*/Verb', "Report @%{ username }…")
|
||||
reportableObjs.push({
|
||||
label: this.$gettextInterpolate(accountLabel, {username: account.preferred_username}),
|
||||
target: {
|
||||
type: 'account',
|
||||
full_username: account.full_username,
|
||||
label: account.full_username,
|
||||
typeLabel: this.$pgettext("*/*/*", 'Account'),
|
||||
}
|
||||
})
|
||||
if (track) {
|
||||
album = track.album
|
||||
artist = track.artist
|
||||
}
|
||||
}
|
||||
if (track) {
|
||||
reportableObjs.push({
|
||||
label: this.$pgettext('*/Moderation/*/Verb', "Report this track…"),
|
||||
target: {
|
||||
type: 'track',
|
||||
id: track.id,
|
||||
label: track.title,
|
||||
typeLabel: this.$pgettext("*/*/*", 'Track'),
|
||||
}
|
||||
})
|
||||
album = track.album
|
||||
artist = track.artist
|
||||
}
|
||||
if (album) {
|
||||
reportableObjs.push({
|
||||
label: this.$pgettext('*/Moderation/*/Verb', "Report this album…"),
|
||||
target: {
|
||||
type: 'album',
|
||||
id: album.id,
|
||||
label: album.title,
|
||||
typeLabel: this.$pgettext("*/*/*", 'Album'),
|
||||
}
|
||||
})
|
||||
if (!artist) {
|
||||
artist = album.artist
|
||||
}
|
||||
}
|
||||
if (artist) {
|
||||
reportableObjs.push({
|
||||
label: this.$pgettext('*/Moderation/*/Verb', "Report this artist…"),
|
||||
target: {
|
||||
type: 'artist',
|
||||
id: artist.id,
|
||||
label: artist.name,
|
||||
typeLabel: this.$pgettext("*/*/*", 'Artist'),
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.playlist) {
|
||||
reportableObjs.push({
|
||||
label: this.$pgettext('*/Moderation/*/Verb', "Report this playlist…"),
|
||||
target: {
|
||||
type: 'playlist',
|
||||
id: this.playlist.id,
|
||||
label: this.playlist.name,
|
||||
typeLabel: this.$pgettext("*/*/*", 'Playlist'),
|
||||
}
|
||||
})
|
||||
}
|
||||
return reportableObjs
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<label v-if="label"><translate translate-context="*/*/*">Category</translate></label>
|
||||
<select class="ui dropdown" :value="value" @change="$emit('input', $event.target.value)">
|
||||
<select class="ui dropdown" :value="value" @change="$emit('input', $event.target.value)" :required="required">
|
||||
<option v-if="empty" disabled value=''></option>
|
||||
<option :value="option.value" v-for="option in allCategories">{{ option.label }}</option>
|
||||
</select>
|
||||
<slot></slot>
|
||||
|
@ -13,7 +14,14 @@ import TranslationsMixin from '@/components/mixins/Translations'
|
|||
import lodash from '@/lodash'
|
||||
export default {
|
||||
mixins: [TranslationsMixin],
|
||||
props: ['value', 'all', 'label'],
|
||||
props: {
|
||||
value: {},
|
||||
all: {},
|
||||
label: {},
|
||||
empty: {},
|
||||
required: {},
|
||||
restrictTo: {default: () => { return [] }}
|
||||
},
|
||||
computed: {
|
||||
allCategories () {
|
||||
let c = []
|
||||
|
@ -25,11 +33,17 @@ export default {
|
|||
},
|
||||
)
|
||||
}
|
||||
let choices
|
||||
if (this.restrictTo.length > 0) {
|
||||
choices = this.restrictTo
|
||||
} else {
|
||||
choices = lodash.keys(this.sharedLabels.fields.report_type.choices)
|
||||
}
|
||||
return c.concat(
|
||||
lodash.keys(this.sharedLabels.fields.report_type.choices).sort().map((v) => {
|
||||
choices.sort().map((v) => {
|
||||
return {
|
||||
value: v,
|
||||
label: this.sharedLabels.fields.report_type.choices[v]
|
||||
label: this.sharedLabels.fields.report_type.choices[v] || v
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<modal @update:show="update" :show="$store.state.moderation.showReportModal">
|
||||
<h2 class="ui header" v-if="target">
|
||||
<translate translate-context="Popup/Moderation/Title/Verb">Do you want to report this object?</translate>
|
||||
<div class="ui sub header">
|
||||
{{ target.typeLabel }} - {{ target.label }}
|
||||
</div>
|
||||
</h2>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<div v-if="errors.length > 0" class="ui negative message">
|
||||
<div class="header"><translate translate-context="Popup/Moderation/Error message">Error while submitting report</translate></div>
|
||||
<ul class="list">
|
||||
<li v-for="error in errors">{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<translate translate-context="*/Moderation/Popup,Paragraph">Use this form to submit a report to our moderation team.</translate>
|
||||
</p>
|
||||
<form v-if="canSubmit" id="report-form" class="ui form" @submit.prevent="submit">
|
||||
<div v-if="!$store.state.auth.authenticated" class="ui inline required field">
|
||||
<label for="report-submitter-email">
|
||||
<translate translate-context="Content/*/*/Noun">Email</translate>
|
||||
</label>
|
||||
<input type="email" v-model="submitterEmail" name="report-submitter-email" id="report-submitter-email" required>
|
||||
</div>
|
||||
<report-category-dropdown
|
||||
class="ui inline required field"
|
||||
v-model="category"
|
||||
:required="true"
|
||||
:empty="true"
|
||||
:restrict-to="allowedCategories"
|
||||
:label="true"></report-category-dropdown>
|
||||
<div class="ui field">
|
||||
<label for="report-summary">
|
||||
<translate translate-context="*/*/Field.Label/Noun">Message</translate>
|
||||
</label>
|
||||
<p>
|
||||
<translate translate-context="*/*/Field,Help">Use this field to provide additional context to the moderator that will handle your report.</translate>
|
||||
</p>
|
||||
<textarea name="report-summary" id="report-summary" rows="8" v-model="summary"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else-if="isLoadingReportTypes" class="ui inline active loader">
|
||||
|
||||
</div>
|
||||
<div v-else class="ui warning message">
|
||||
<div class="header">
|
||||
<translate translate-context="Popup/Moderation/Error message">Anonymous reports are disabled, please sign-in to submit a report.</translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui cancel button"><translate translate-context="*/*/Button.Label/Verb">Cancel</translate></div>
|
||||
<button
|
||||
v-if="canSubmit"
|
||||
:class="['ui', 'green', {loading: isLoading}, 'button']"
|
||||
type="submit" form="report-form">
|
||||
<translate translate-context="Popup/*/Button.Label">Submit report</translate>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import logger from '@/logging'
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import ReportCategoryDropdown from '@/components/moderation/ReportCategoryDropdown'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
ReportCategoryDropdown,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
formKey: String(new Date()),
|
||||
errors: [],
|
||||
isLoading: false,
|
||||
isLoadingReportTypes: false,
|
||||
summary: '',
|
||||
submitterEmail: '',
|
||||
category: null,
|
||||
reportTypes: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
target: state => state.moderation.reportModalTarget,
|
||||
}),
|
||||
allowedCategories () {
|
||||
if (this.$store.state.auth.authenticated) {
|
||||
return []
|
||||
}
|
||||
return this.reportTypes.filter((t) => {
|
||||
return t.anonymous === true
|
||||
}).map((c) => {
|
||||
return c.type
|
||||
})
|
||||
|
||||
},
|
||||
canSubmit () {
|
||||
if (this.$store.state.auth.authenticated) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.allowedCategories.length > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (v) {
|
||||
this.$store.commit('moderation/showReportModal', v)
|
||||
this.errors = []
|
||||
},
|
||||
submit () {
|
||||
let self = this
|
||||
self.isLoading = true
|
||||
let payload = {
|
||||
target: this.target,
|
||||
summary: this.summary,
|
||||
type: this.category,
|
||||
}
|
||||
if (!this.$store.state.auth.authenticated) {
|
||||
payload.submitter_email = this.submitterEmail
|
||||
}
|
||||
return axios.post('moderation/reports/', payload).then(response => {
|
||||
self.update(false)
|
||||
self.isLoading = false
|
||||
let msg = this.$pgettext('*/Moderation/Message', 'Report successfully submitted, thank you')
|
||||
self.$store.commit('moderation/contentFilter', response.data)
|
||||
self.$store.commit('ui/addMessage', {
|
||||
content: msg,
|
||||
date: new Date()
|
||||
})
|
||||
self.summary = ''
|
||||
self.category = ''
|
||||
}, error => {
|
||||
self.errors = error.backendErrors
|
||||
self.isLoading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.moderation.showReportModal': function (v) {
|
||||
if (!v || this.$store.state.auth.authenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
let self = this
|
||||
self.isLoadingReportTypes = true
|
||||
axios.get('instance/nodeinfo/2.0/').then(response => {
|
||||
self.isLoadingReportTypes = false
|
||||
self.reportTypes = response.data.metadata.reportTypes || []
|
||||
}, error => {
|
||||
self.isLoadingReportTypes = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -5,8 +5,18 @@
|
|||
<div class="content">
|
||||
<div class="header">
|
||||
<div class="right floated">
|
||||
<play-button :is-playable="playlist.is_playable" :icon-only="true" class="ui inline" :button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]" :playlist="playlist"></play-button>
|
||||
<play-button :is-playable="playlist.is_playable" class="basic inline icon" :dropdown-only="true" :dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']" :playlist="playlist"></play-button>
|
||||
<play-button
|
||||
:is-playable="playlist.is_playable"
|
||||
:icon-only="true" class="ui inline"
|
||||
:button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]"
|
||||
:playlist="playlist"></play-button>
|
||||
<play-button
|
||||
:is-playable="playlist.is_playable"
|
||||
class="basic inline icon"
|
||||
:dropdown-only="true"
|
||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']"
|
||||
:account="playlist.actor"
|
||||
:playlist="playlist"></play-button>
|
||||
</div>
|
||||
<router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
|
||||
{{ playlist.name | truncate(30) }}
|
||||
|
|
|
@ -7,16 +7,24 @@ export default {
|
|||
state: {
|
||||
filters: [],
|
||||
showFilterModal: false,
|
||||
showReportModal: false,
|
||||
lastUpdate: new Date(),
|
||||
filterModalTarget: {
|
||||
type: null,
|
||||
target: null,
|
||||
},
|
||||
reportModalTarget: {
|
||||
type: null,
|
||||
target: null,
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
filterModalTarget (state, value) {
|
||||
state.filterModalTarget = value
|
||||
},
|
||||
reportModalTarget (state, value) {
|
||||
state.reportModalTarget = value
|
||||
},
|
||||
empty (state) {
|
||||
state.filters = []
|
||||
},
|
||||
|
@ -35,10 +43,21 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
showReportModal (state, value) {
|
||||
state.showReportModal = value
|
||||
if (!value) {
|
||||
state.reportModalTarget = {
|
||||
type: null,
|
||||
target: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
reset (state) {
|
||||
state.filters = []
|
||||
state.filterModalTarget = null
|
||||
state.showFilterModal = false
|
||||
state.showReportModal = false
|
||||
state.reportModalTarget = {}
|
||||
},
|
||||
deleteContentFilter (state, uuid) {
|
||||
state.filters = state.filters.filter((e) => {
|
||||
|
@ -61,6 +80,10 @@ export default {
|
|||
commit('filterModalTarget', payload)
|
||||
commit('showFilterModal', true)
|
||||
},
|
||||
report ({commit}, payload) {
|
||||
commit('reportModalTarget', payload)
|
||||
commit('showReportModal', true)
|
||||
},
|
||||
fetchContentFilters ({dispatch, state, commit, rootState}, url) {
|
||||
let params = {}
|
||||
let promise
|
||||
|
|
Loading…
Reference in New Issue