Report UI (end-user)

environments/review-docs-rate-jr6phc/deployments/2479
Eliot Berriot 2019-09-09 11:10:25 +02:00
rodzic 1a8edf27b3
commit 33d1f879cf
24 zmienionych plików z 519 dodań i 32 usunięć

Wyświetl plik

@ -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):

Wyświetl plik

@ -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

Wyświetl plik

@ -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:

Wyświetl plik

@ -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):

Wyświetl plik

@ -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:

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -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: {

Wyświetl plik

@ -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>

Wyświetl plik

@ -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,

Wyświetl plik

@ -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,

Wyświetl plik

@ -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,

Wyświetl plik

@ -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>

Wyświetl plik

@ -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
}
})
)

Wyświetl plik

@ -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>

Wyświetl plik

@ -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) }}

Wyświetl plik

@ -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