See #170: admin UI for channels, reporting channels

environments/review-docs-confi-pvdyb2/deployments/4739
Eliot Berriot 2020-03-31 10:45:41 +02:00
rodzic ae52969efe
commit 102c90d499
32 zmienionych plików z 1106 dodań i 77 usunięć

Wyświetl plik

@ -69,6 +69,15 @@ class Channel(models.Model):
objects = ChannelQuerySet.as_manager()
@property
def fid(self):
if not self.is_external_rss:
return self.actor.fid
@property
def is_external_rss(self):
return self.actor.preferred_username.startswith("rssfeed-")
def get_absolute_url(self):
suffix = self.uuid
if self.actor.is_local:
@ -78,9 +87,7 @@ class Channel(models.Model):
return federation_utils.full_url("/channels/{}".format(suffix))
def get_rss_url(self):
if not self.artist.is_local or self.actor.preferred_username.startswith(
"rssfeed-"
):
if not self.artist.is_local or self.is_external_rss:
return self.rss_url
return federation_utils.full_url(
@ -90,10 +97,6 @@ class Channel(models.Model):
)
)
@property
def fid(self):
return self.actor.fid
def generate_actor(username, **kwargs):
actor_data = user_models.get_actor_data(username, **kwargs)

Wyświetl plik

@ -145,6 +145,7 @@ class Domain(models.Model):
actors=models.Count("actors", distinct=True),
outbox_activities=models.Count("actors__outbox_activities", distinct=True),
libraries=models.Count("actors__libraries", distinct=True),
channels=models.Count("actors__owned_channels", distinct=True),
received_library_follows=models.Count(
"actors__libraries__received_follows", distinct=True
),
@ -283,6 +284,7 @@ class Actor(models.Model):
data = Actor.objects.filter(pk=self.pk).aggregate(
outbox_activities=models.Count("outbox_activities", distinct=True),
libraries=models.Count("libraries", distinct=True),
channels=models.Count("owned_channels", distinct=True),
received_library_follows=models.Count(
"libraries__received_follows", distinct=True
),

Wyświetl plik

@ -482,6 +482,8 @@ def inbox_flag(payload, context):
@outbox.register({"type": "Flag"})
def outbox_flag(context):
report = context["report"]
if not report.target or not report.target.fid:
return
actor = actors.get_service_actor()
serializer = serializers.FlagSerializer(report)
yield {

Wyświetl plik

@ -266,5 +266,11 @@ def get_object_by_fid(fid, local=None):
if not result:
raise ObjectDoesNotExist()
model = apps.get_model(*result["__type"].split("."))
instance = model.objects.get(fid=fid)
if model._meta.label == "federation.Actor":
channel = instance.get_channel()
if channel:
return channel
return apps.get_model(*result["__type"].split(".")).objects.get(fid=fid)
return instance

Wyświetl plik

@ -8,6 +8,7 @@ from funkwhale_api.common import fields
from funkwhale_api.common import filters as common_filters
from funkwhale_api.common import search
from funkwhale_api.audio import models as audio_models
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.moderation import models as moderation_models
@ -34,6 +35,34 @@ def get_actor_filter(actor_field):
return {"field": ActorField(), "handler": handler}
class ManageChannelFilterSet(filters.FilterSet):
q = fields.SmartSearchFilter(
config=search.SearchConfig(
search_fields={
"name": {"to": "artist__name"},
"username": {"to": "artist__name"},
"fid": {"to": "artist__fid"},
"rss": {"to": "rss_url"},
},
filter_fields={
"uuid": {"to": "uuid"},
"category": {"to": "artist__content_category"},
"domain": {
"handler": lambda v: federation_utils.get_domain_query_from_url(
v, url_field="attributed_to__fid"
)
},
"tag": {"to": "artist__tagged_items__tag__name", "distinct": True},
"account": get_actor_filter("attributed_to"),
},
)
)
class Meta:
model = audio_models.Channel
fields = ["q"]
class ManageArtistFilterSet(filters.FilterSet):
q = fields.SmartSearchFilter(
config=search.SearchConfig(
@ -52,6 +81,7 @@ class ManageArtistFilterSet(filters.FilterSet):
"field": forms.IntegerField(),
"distinct": True,
},
"category": {"to": "content_category"},
"tag": {"to": "tagged_items__tag__name", "distinct": True},
},
)
@ -59,7 +89,7 @@ class ManageArtistFilterSet(filters.FilterSet):
class Meta:
model = music_models.Artist
fields = ["q", "name", "mbid", "fid"]
fields = ["q", "name", "mbid", "fid", "content_category"]
class ManageAlbumFilterSet(filters.FilterSet):

Wyświetl plik

@ -3,6 +3,7 @@ from django.db import transaction
from rest_framework import serializers
from funkwhale_api.audio import models as audio_models
from funkwhale_api.common import fields as common_fields
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils
@ -386,26 +387,39 @@ class ManageNestedAlbumSerializer(ManageBaseAlbumSerializer):
class ManageArtistSerializer(
music_serializers.OptionalDescriptionMixin, ManageBaseArtistSerializer
):
albums = ManageNestedAlbumSerializer(many=True)
tracks = ManageNestedTrackSerializer(many=True)
attributed_to = ManageBaseActorSerializer()
tags = serializers.SerializerMethodField()
tracks_count = serializers.SerializerMethodField()
albums_count = serializers.SerializerMethodField()
channel = serializers.SerializerMethodField()
cover = music_serializers.cover_field
class Meta:
model = music_models.Artist
fields = ManageBaseArtistSerializer.Meta.fields + [
"albums",
"tracks",
"tracks_count",
"albums_count",
"attributed_to",
"tags",
"cover",
"channel",
"content_category",
]
def get_tracks_count(self, obj):
return getattr(obj, "_tracks_count", None)
def get_albums_count(self, obj):
return getattr(obj, "_albums_count", None)
def get_tags(self, obj):
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
return [ti.tag.name for ti in tagged_items]
def get_channel(self, obj):
if "channel" in obj._state.fields_cache and obj.get_channel():
return str(obj.channel.uuid)
class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
pass
@ -743,3 +757,23 @@ class ManageUserRequestSerializer(serializers.ModelSerializer):
def get_notes(self, o):
notes = getattr(o, "_prefetched_notes", [])
return ManageBaseNoteSerializer(notes, many=True).data
class ManageChannelSerializer(serializers.ModelSerializer):
attributed_to = ManageBaseActorSerializer()
actor = ManageBaseActorSerializer()
artist = ManageArtistSerializer()
class Meta:
model = audio_models.Channel
fields = [
"id",
"uuid",
"creation_date",
"artist",
"attributed_to",
"actor",
"rss_url",
"metadata",
]
read_only_fields = fields

Wyświetl plik

@ -27,6 +27,7 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
other_router = routers.OptionalSlashRouter()
other_router.register(r"accounts", views.ManageActorViewSet, "accounts")
other_router.register(r"channels", views.ManageChannelViewSet, "channels")
other_router.register(r"tags", views.ManageTagViewSet, "tags")
urlpatterns = [

Wyświetl plik

@ -6,12 +6,15 @@ from django.db.models import Count, Prefetch, Q, Sum, OuterRef, Subquery
from django.db.models.functions import Coalesce, Length
from django.shortcuts import get_object_or_404
from funkwhale_api.audio import models as audio_models
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.common import models as common_models
from funkwhale_api.common import preferences, decorators
from funkwhale_api.common import utils as common_utils
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.history import models as history_models
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
@ -25,37 +28,39 @@ from funkwhale_api.users import models as users_models
from . import filters, serializers
def get_stats(tracks, target):
data = {}
def get_stats(tracks, target, ignore_fields=[]):
tracks = list(tracks.values_list("pk", flat=True))
uploads = music_models.Upload.objects.filter(track__in=tracks)
data["listenings"] = history_models.Listening.objects.filter(
track__in=tracks
).count()
data["mutations"] = common_models.Mutation.objects.get_for_target(target).count()
data["playlists"] = (
playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
.values_list("playlist", flat=True)
.distinct()
.count()
)
data["track_favorites"] = favorites_models.TrackFavorite.objects.filter(
track__in=tracks
).count()
data["libraries"] = (
uploads.filter(library__channel=None)
.values_list("library", flat=True)
.distinct()
.count()
)
data["channels"] = (
uploads.exclude(library__channel=None)
.values_list("library", flat=True)
.distinct()
.count()
)
data["uploads"] = uploads.count()
data["reports"] = moderation_models.Report.objects.get_for_target(target).count()
fields = {
"listenings": history_models.Listening.objects.filter(track__in=tracks),
"mutations": common_models.Mutation.objects.get_for_target(target),
"playlists": (
playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
.values_list("playlist", flat=True)
.distinct()
),
"track_favorites": (
favorites_models.TrackFavorite.objects.filter(track__in=tracks)
),
"libraries": (
uploads.filter(library__channel=None)
.values_list("library", flat=True)
.distinct()
),
"channels": (
uploads.exclude(library__channel=None)
.values_list("library", flat=True)
.distinct()
),
"uploads": uploads,
"reports": moderation_models.Report.objects.get_for_target(target),
}
data = {}
for key, qs in fields.items():
if key in ignore_fields:
continue
data[key] = qs.count()
data.update(get_media_stats(uploads))
return data
@ -78,17 +83,10 @@ class ManageArtistViewSet(
queryset = (
music_models.Artist.objects.all()
.order_by("-id")
.select_related("attributed_to", "attachment_cover",)
.prefetch_related(
"tracks",
Prefetch(
"albums",
queryset=music_models.Album.objects.select_related(
"attachment_cover"
).annotate(tracks_count=Count("tracks")),
),
music_views.TAG_PREFETCH,
)
.select_related("attributed_to", "attachment_cover", "channel")
.annotate(_tracks_count=Count("tracks"))
.annotate(_albums_count=Count("albums"))
.prefetch_related(music_views.TAG_PREFETCH)
)
serializer_class = serializers.ManageArtistSerializer
filterset_class = filters.ManageArtistFilterSet
@ -661,3 +659,64 @@ class ManageUserRequestViewSet(
)
else:
serializer.save()
class ManageChannelViewSet(
MultipleLookupDetailMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
url_lookups = [
{
"lookup_field": "uuid",
"validator": serializers.serializers.UUIDField().to_internal_value,
},
{
"lookup_field": "username",
"validator": federation_utils.get_actor_data_from_username,
"get_query": lambda v: Q(
actor__domain=v["domain"],
actor__preferred_username__iexact=v["username"],
),
},
]
queryset = (
audio_models.Channel.objects.all()
.order_by("-id")
.select_related("attributed_to", "actor",)
.prefetch_related(
Prefetch(
"artist",
queryset=(
music_models.Artist.objects.all()
.order_by("-id")
.select_related("attributed_to", "attachment_cover", "channel")
.annotate(_tracks_count=Count("tracks"))
.annotate(_albums_count=Count("albums"))
.prefetch_related(music_views.TAG_PREFETCH)
),
)
)
)
serializer_class = serializers.ManageChannelSerializer
filterset_class = filters.ManageChannelFilterSet
required_scope = "instance:libraries"
ordering_fields = ["creation_date", "name"]
@rest_decorators.action(methods=["get"], detail=True)
def stats(self, request, *args, **kwargs):
channel = self.get_object()
tracks = music_models.Track.objects.filter(
Q(artist=channel.artist) | Q(album__artist=channel.artist)
)
data = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
data["follows"] = channel.actor.received_follows.count()
return response.Response(data, status=200)
def get_serializer_context(self):
context = super().get_serializer_context()
context["description"] = self.action in ["retrieve", "create", "update"]
return context

Wyświetl plik

@ -6,6 +6,7 @@ from django.core.serializers.json import DjangoJSONEncoder
import persisting_theory
from rest_framework import serializers
from funkwhale_api.audio import models as audio_models
from funkwhale_api.common import fields as common_fields
from funkwhale_api.common import preferences
from funkwhale_api.federation import models as federation_models
@ -61,20 +62,36 @@ class UserFilterSerializer(serializers.ModelSerializer):
state_serializers = persisting_theory.Registry()
class DescriptionStateMixin(object):
def get_description(self, o):
if o.description:
return o.description.text
TAGS_FIELD = serializers.ListField(source="get_tags")
@state_serializers.register(name="music.Artist")
class ArtistStateSerializer(serializers.ModelSerializer):
class ArtistStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
tags = TAGS_FIELD
class Meta:
model = music_models.Artist
fields = ["id", "name", "mbid", "fid", "creation_date", "uuid", "tags"]
fields = [
"id",
"name",
"mbid",
"fid",
"creation_date",
"uuid",
"tags",
"content_category",
"description",
]
@state_serializers.register(name="music.Album")
class AlbumStateSerializer(serializers.ModelSerializer):
class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
tags = TAGS_FIELD
artist = ArtistStateSerializer()
@ -90,11 +107,12 @@ class AlbumStateSerializer(serializers.ModelSerializer):
"artist",
"release_date",
"tags",
"description",
]
@state_serializers.register(name="music.Track")
class TrackStateSerializer(serializers.ModelSerializer):
class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
tags = TAGS_FIELD
artist = ArtistStateSerializer()
album = AlbumStateSerializer()
@ -115,6 +133,7 @@ class TrackStateSerializer(serializers.ModelSerializer):
"license",
"copyright",
"tags",
"description",
]
@ -156,6 +175,36 @@ class ActorStateSerializer(serializers.ModelSerializer):
]
@state_serializers.register(name="audio.Channel")
class ChannelStateSerializer(serializers.ModelSerializer):
rss_url = serializers.CharField(source="get_rss_url")
name = serializers.CharField(source="artist.name")
full_username = serializers.CharField(source="actor.full_username")
domain = serializers.CharField(source="actor.domain_id")
description = serializers.SerializerMethodField()
tags = serializers.ListField(source="artist.get_tags")
content_category = serializers.CharField(source="artist.content_category")
class Meta:
model = audio_models.Channel
fields = [
"uuid",
"name",
"rss_url",
"metadata",
"full_username",
"description",
"domain",
"creation_date",
"tags",
"content_category",
]
def get_description(self, o):
if o.artist.description:
return o.artist.description.text
def get_actor_query(attr, value):
data = federation_utils.get_actor_data_from_username(value)
return federation_utils.get_actor_from_username_data_query(None, data)
@ -163,6 +212,7 @@ def get_actor_query(attr, value):
def get_target_owner(target):
mapping = {
audio_models.Channel: lambda t: t.attributed_to,
music_models.Artist: lambda t: t.attributed_to,
music_models.Album: lambda t: t.attributed_to,
music_models.Track: lambda t: t.attributed_to,
@ -175,6 +225,11 @@ def get_target_owner(target):
TARGET_CONFIG = {
"channel": {
"queryset": audio_models.Channel.objects.all(),
"id_attr": "uuid",
"id_field": serializers.UUIDField(),
},
"artist": {"queryset": music_models.Artist.objects.all()},
"album": {"queryset": music_models.Album.objects.all()},
"track": {"queryset": music_models.Track.objects.all()},

Wyświetl plik

@ -126,6 +126,7 @@ def test_domain_stats(factories):
"libraries": 0,
"tracks": 0,
"albums": 0,
"channels": 0,
"uploads": 0,
"artists": 0,
"outbox_activities": 0,
@ -148,6 +149,7 @@ def test_actor_stats(factories):
"uploads": 0,
"artists": 0,
"reports": 0,
"channels": 0,
"requests": 0,
"outbox_activities": 0,
"received_library_follows": 0,

Wyświetl plik

@ -844,6 +844,7 @@ def test_inbox_delete_actor_doesnt_delete_local_actor(factories):
@pytest.mark.parametrize(
"factory_name, factory_kwargs",
[
("audio.Channel", {"local": True}),
("federation.Actor", {"local": True}),
("music.Artist", {"local": True}),
("music.Album", {"local": True}),
@ -885,6 +886,7 @@ def test_inbox_flag(factory_name, factory_kwargs, factories, mocker):
@pytest.mark.parametrize(
"factory_name, factory_kwargs",
[
("audio.Channel", {"local": True}),
("federation.Actor", {"local": True}),
("music.Artist", {"local": True}),
("music.Album", {"local": True}),

Wyświetl plik

@ -207,3 +207,9 @@ def test_get_obj_by_fid(factory_name, factories):
obj = factories[factory_name]()
factories[factory_name]()
assert utils.get_object_by_fid(obj.fid) == obj
def test_get_channel_by_fid(factories):
obj = factories["audio.Channel"]()
factories["audio.Channel"]()
assert utils.get_object_by_fid(obj.actor.fid) == obj

Wyświetl plik

@ -287,8 +287,11 @@ def test_instance_policy_serializer_purges_target_actor(
def test_manage_artist_serializer(factories, now, to_api_date):
artist = factories["music.Artist"](attributed=True, with_cover=True)
track = factories["music.Track"](artist=artist)
album = factories["music.Album"](artist=artist)
channel = factories["audio.Channel"](artist=artist)
# put channel in cache
artist.get_channel()
setattr(artist, "_tracks_count", 12)
setattr(artist, "_albums_count", 13)
expected = {
"id": artist.id,
"domain": artist.domain_name,
@ -297,12 +300,14 @@ def test_manage_artist_serializer(factories, now, to_api_date):
"name": artist.name,
"mbid": artist.mbid,
"creation_date": to_api_date(artist.creation_date),
"albums": [serializers.ManageNestedAlbumSerializer(album).data],
"tracks": [serializers.ManageNestedTrackSerializer(track).data],
"tracks_count": 12,
"albums_count": 13,
"attributed_to": serializers.ManageBaseActorSerializer(
artist.attributed_to
).data,
"tags": [],
"channel": str(channel.uuid),
"content_category": artist.content_category,
"cover": common_serializers.AttachmentSerializer(artist.attachment_cover).data,
}
s = serializers.ManageArtistSerializer(artist)
@ -585,3 +590,22 @@ def test_manage_user_request_serializer(factories, to_api_date):
s = serializers.ManageUserRequestSerializer(user_request)
assert s.data == expected
def test_manage_channel_serializer(factories, now, to_api_date):
channel = factories["audio.Channel"]()
expected = {
"id": channel.id,
"uuid": channel.uuid,
"artist": serializers.ManageArtistSerializer(channel.artist).data,
"actor": serializers.ManageBaseActorSerializer(channel.actor).data,
"attributed_to": serializers.ManageBaseActorSerializer(
channel.attributed_to
).data,
"creation_date": to_api_date(channel.creation_date),
"rss_url": channel.get_rss_url(),
"metadata": channel.metadata,
}
s = serializers.ManageChannelSerializer(channel)
assert s.data == expected

Wyświetl plik

@ -599,3 +599,50 @@ def test_user_request_update_status_assigns(factories, superuser_api_client, moc
new_status="refused",
old_status="pending",
)
def test_channel_list(factories, superuser_api_client, settings):
channel = factories["audio.Channel"]()
url = reverse("api:v1:manage:channels-list")
response = superuser_api_client.get(url)
assert response.status_code == 200
assert response.data["count"] == 1
assert response.data["results"][0]["id"] == channel.id
def test_channel_detail(factories, superuser_api_client):
channel = factories["audio.Channel"]()
url = reverse("api:v1:manage:channels-detail", kwargs={"composite": channel.uuid})
response = superuser_api_client.get(url)
assert response.status_code == 200
assert response.data["id"] == channel.id
def test_channel_delete(factories, superuser_api_client, mocker):
channel = factories["audio.Channel"]()
url = reverse("api:v1:manage:channels-detail", kwargs={"composite": channel.uuid})
response = superuser_api_client.delete(url)
assert response.status_code == 204
def test_channel_detail_stats(factories, superuser_api_client):
channel = factories["audio.Channel"]()
url = reverse("api:v1:manage:channels-stats", kwargs={"composite": channel.uuid})
response = superuser_api_client.get(url)
expected = {
"uploads": 0,
"playlists": 0,
"listenings": 0,
"mutations": 0,
"reports": 0,
"follows": 0,
"track_favorites": 0,
"media_total_size": 0,
"media_downloaded_size": 0,
}
assert response.status_code == 200
assert response.data == expected

Wyświetl plik

@ -52,6 +52,7 @@ def test_user_filter_serializer_save(factories):
"full_username",
serializers.ActorStateSerializer,
),
("audio.Channel", "channel", "uuid", serializers.ChannelStateSerializer),
],
)
def test_report_federated_entity_serializer_save(
@ -161,6 +162,7 @@ def test_report_serializer_save_anonymous(factories, mocker):
("music.Library", {}, "actor"),
("playlists.Playlist", {"user__with_actor": True}, "user.actor"),
("federation.Actor", {}, "self"),
("audio.Channel", {}, "attributed_to"),
],
)
def test_get_target_owner(factory_name, factory_kwargs, owner_field, factories):

Wyświetl plik

@ -406,9 +406,9 @@ export default {
},
'serviceWorker.updateAvailable': {
handler (v) {
// if (!v) {
// return
// }
if (!v) {
return
}
let self = this
this.$store.commit('ui/addMessage', {
content: this.$pgettext("App/Message/Paragraph", "A new version of the app is available."),

Wyświetl plik

@ -30,7 +30,11 @@
:title="updatedTitle">
{{ object.artist.modification_date | fromNow }}
</time>
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="true" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :artist="object.artist"></play-button>
<play-button
class="right floated basic icon"
:dropdown-only="true"
:is-playable="true"
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :artist="object.artist" :channel="object" :account="object.attributed_to"></play-button>
</div>
</div>
</template>

Wyświetl plik

@ -35,7 +35,7 @@
<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})"
v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})"
:key="obj.target.type + obj.target.id"
class="item basic"
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
@ -69,6 +69,7 @@ export default {
artist: {type: Object, required: false},
album: {type: Object, required: false},
library: {type: Object, required: false},
channel: {type: Object, required: false},
isPlayable: {type: Boolean, required: false, default: null}
},
data () {

Wyświetl plik

@ -0,0 +1,224 @@
<template>
<div>
<div class="ui inline form">
<div class="fields">
<div class="ui six wide field">
<label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
<form @submit.prevent="search.query = $refs.search.value">
<input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
</form>
</div>
<div class="field">
<label><translate translate-context="*/*/*">Category</translate></label>
<select class="ui dropdown" @change="addSearchToken('category', $event.target.value)" :value="getTokenValue('category', '')">
<option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
<option value="podcast">{{ sharedLabels.fields.content_category.choices.podcast }}</option>
<option value="music">{{ sharedLabels.fields.content_category.choices.music }}</option>
<option value="other">{{ sharedLabels.fields.content_category.choices.other }}</option>
</select>
</div>
<div class="field">
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
</div>
<div class="field">
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
</select>
</div>
</div>
</div>
<div class="dimmable">
<div v-if="isLoading" class="ui active inverted dimmer">
<div class="ui loader"></div>
</div>
<action-table
v-if="result"
@action-launched="fetchData"
:objects-data="result"
:actions="actions"
action-url="manage/library/artists/action/"
:filters="actionFilters">
<template slot="header-cells">
<th><translate translate-context="*/*/*/Noun">Name</translate></th>
<th><translate translate-context="*/*/*/Noun">Account</translate></th>
<th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
<th><translate translate-context="*/*/*">Albums</translate></th>
<th><translate translate-context="*/*/*">Tracks</translate></th>
<th><translate translate-context="Content/*/*/Noun">Creation date</translate></th>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<router-link :to="{name: 'manage.channels.detail', params: {id: scope.obj.actor.full_username }}">{{ scope.obj.artist.name }}</router-link>
</td>
<td>
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.attributed_to.full_username }}">
<i class="wrench icon"></i>
</router-link>
<span role="button" class="discrete link" @click="addSearchToken('account', scope.obj.attributed_to.full_username)" :title="scope.obj.attributed_to.full_username">{{ scope.obj.attributed_to.preferred_username }}</span>
</td>
<td>
<template v-if="!scope.obj.is_local">
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.attributed_to.domain }}">
<i class="wrench icon"></i>
</router-link>
<span role="button" class="discrete link" @click="addSearchToken('domain', scope.obj.attributed_to.domain)" :title="scope.obj.attributed_to.domain">{{ scope.obj.attributed_to.domain }}</span>
</template>
<span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.attributed_to.domain)">
<i class="home icon"></i>
<translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
</span>
</td>
<td>
{{ scope.obj.artist.albums_count }}
</td>
<td>
{{ scope.obj.artist.tracks_count }}
</td>
<td>
<human-date :date="scope.obj.creation_date"></human-date>
</td>
</template>
</action-table>
</div>
<div>
<pagination
v-if="result && result.count > paginateBy"
@page-changed="selectPage"
:compact="true"
:current="page"
:paginate-by="paginateBy"
:total="result.count"
></pagination>
<span v-if="result && result.results.length > 0">
<translate translate-context="Content/*/Paragraph"
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</span>
</div>
</div>
</template>
<script>
import axios from 'axios'
import _ from '@/lodash'
import time from '@/utils/time'
import {normalizeQuery, parseTokens} from '@/search'
import Pagination from '@/components/Pagination'
import ActionTable from '@/components/common/ActionTable'
import OrderingMixin from '@/components/mixins/Ordering'
import TranslationsMixin from '@/components/mixins/Translations'
import SmartSearchMixin from '@/components/mixins/SmartSearch'
export default {
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
props: {
filters: {type: Object, required: false},
},
components: {
Pagination,
ActionTable
},
data () {
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
return {
time,
isLoading: false,
result: null,
page: 1,
paginateBy: 50,
search: {
query: this.defaultQuery,
tokens: parseTokens(normalizeQuery(this.defaultQuery))
},
orderingDirection: defaultOrdering.direction || '+',
ordering: defaultOrdering.field,
orderingOptions: [
['creation_date', 'creation_date'],
["name", "name"],
]
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
let params = _.merge({
'page': this.page,
'page_size': this.paginateBy,
'q': this.search.query,
'ordering': this.getOrderingAsString()
}, this.filters)
let self = this
self.isLoading = true
self.checked = []
axios.get('/manage/channels/', {params: params}).then((response) => {
self.result = response.data
self.isLoading = false
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
},
selectPage: function (page) {
this.page = page
},
},
computed: {
labels () {
return {
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, name, account…')
}
},
actionFilters () {
var currentFilters = {
q: this.search.query
}
if (this.filters) {
return _.merge(currentFilters, this.filters)
} else {
return currentFilters
}
},
actions () {
// let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
// let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected artist will be removed, as well as associated uploads, tracks, albums, favorites and listening history. This action is irreversible.')
return [
// {
// name: 'delete',
// label: deleteLabel,
// confirmationMessage: confirmationMessage,
// isDangerous: true,
// allowAll: false,
// confirmColor: 'red',
// },
]
}
},
watch: {
search (newValue) {
this.page = 1
this.fetchData()
},
page () {
this.fetchData()
},
ordering () {
this.fetchData()
},
orderingDirection () {
this.fetchData()
}
}
}
</script>

Wyświetl plik

@ -8,6 +8,15 @@
<input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
</form>
</div>
<div class="field">
<label><translate translate-context="*/*/*">Category</translate></label>
<select class="ui dropdown" @change="addSearchToken('category', $event.target.value)" :value="getTokenValue('category', '')">
<option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
<option value="podcast">{{ sharedLabels.fields.content_category.choices.podcast }}</option>
<option value="music">{{ sharedLabels.fields.content_category.choices.music }}</option>
<option value="other">{{ sharedLabels.fields.content_category.choices.other }}</option>
</select>
</div>
<div class="field">
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
<select class="ui dropdown" v-model="ordering">
@ -45,7 +54,9 @@
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<router-link :to="{name: 'manage.library.artists.detail', params: {id: scope.obj.id }}">{{ scope.obj.name }}</router-link>
<router-link :to="getUrl(scope.obj)">
{{ scope.obj.name }}
</router-link>
</td>
<td>
<template v-if="!scope.obj.is_local">
@ -60,10 +71,10 @@
</span>
</td>
<td>
{{ scope.obj.albums.length }}
{{ scope.obj.albums_count }}
</td>
<td>
{{ scope.obj.tracks.length }}
{{ scope.obj.tracks_count }}
</td>
<td>
<human-date :date="scope.obj.creation_date"></human-date>
@ -136,6 +147,12 @@ export default {
this.fetchData()
},
methods: {
getUrl (artist) {
if (artist.channel) {
return {name: 'manage.channels.detail', params: {id: artist.channel }}
}
return {name: 'manage.library.artists.detail', params: {id: artist.id }}
},
fetchData () {
let params = _.merge({
'page': this.page,

Wyświetl plik

@ -49,7 +49,20 @@ export default {
artist = album.artist
}
}
if (artist) {
if (channel) {
reportableObjs.push({
label: this.$pgettext('*/Moderation/*/Verb', "Report this channel…"),
target: {
type: 'channel',
uuid: channel.uuid,
label: channel.artist.name,
_obj: channel,
typeLabel: this.$pgettext("*/*/*", 'Channel'),
}
})
}
else if (artist) {
reportableObjs.push({
label: this.$pgettext('*/Moderation/*/Verb', "Report this artist…"),
target: {

Wyświetl plik

@ -56,6 +56,14 @@ export default {
summary: {
label: this.$pgettext('Content/Account/*', 'Bio'),
},
content_category: {
label: this.$pgettext('Content/*/Dropdown.Label/Noun', 'Content category'),
choices: {
podcast: this.$pgettext('Content/*/Dropdown', 'Podcast'),
music: this.$pgettext('*/*/*', 'Music'),
other: this.$pgettext('*/*/*', 'Other'),
},
}
},
filters: {
creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),

Wyświetl plik

@ -140,6 +140,9 @@ export default {
return
}
let fid = this.target._obj.fid
if (this.target.type === 'channel' && this.target._obj.actor ) {
fid = this.target._obj.actor.fid
}
if (!fid) {
return this.$store.getters['instance/domain']
}

Wyświetl plik

@ -179,6 +179,7 @@ export default {
label: this.$pgettext('*/*/*/Noun', 'Account'),
icon: 'user',
urls: {
getDetail: (obj) => { return {name: 'profile.full.overview', params: {username: obj.preferred_username, domain: obj.domain}}},
getAdminDetail: (obj) => { return {name: 'manage.moderation.accounts.detail', params: {id: `${obj.preferred_username}@${obj.domain}`}}}
},
moderatedFields: [
@ -194,6 +195,33 @@ export default {
},
]
},
channel: {
label: this.$pgettext('*/*/*', 'Channel'),
icon: 'stream',
urls: {
getDetail: (obj) => { return {name: 'channels.detail', params: {id: obj.uuid}}},
getAdminDetail: (obj) => { return {name: 'manage.channels.detail', params: {id: obj.uuid}}}
},
moderatedFields: [
{
id: 'name',
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
{
id: 'creation_date',
label: this.$pgettext('Content/*/*/Noun', 'Creation date'),
getValue: (obj) => { return obj.creation_date }
},
{
id: 'tags',
type: 'tags',
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
},
]
},
}
},

Wyświetl plik

@ -298,6 +298,28 @@ export default new Router({
},
{
path: "channels",
name: "manage.channels",
component: () =>
import(
/* webpackChunkName: "admin" */ "@/views/admin/ChannelsList"
),
props: route => {
return {
defaultQuery: route.query.q
}
}
},
{
path: "channels/:id",
name: "manage.channels.detail",
component: () =>
import(
/* webpackChunkName: "admin" */ "@/views/admin/ChannelDetail"
),
props: true
},
{
path: "albums",
name: "manage.library.albums",
component: () =>
import(

Wyświetl plik

@ -0,0 +1,369 @@
<template>
<main>
<div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<template v-if="object">
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.artist.name">
<div class="ui stackable one column grid">
<div class="ui column">
<div class="segment-content">
<h2 class="ui header">
<img v-if="object.artist.cover && object.artist.cover.square_crop" v-lazy="$store.getters['instance/absoluteUrl'](object.artist.cover.square_crop)">
<img v-else src="../../assets/audio/default-cover.png">
<div class="content">
{{ object.artist.name | truncate(100) }}
<div class="sub header">
<template v-if="object.artist.is_local">
<span class="ui tiny teal label">
<i class="home icon"></i>
<translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
</span>
&nbsp;
</template>
</div>
</div>
</h2>
<template v-if="object.artist.tags && object.artist.tags.length > 0">
<tags-list :limit="5" detail-route="manage.library.tags.detail" :tags="object.artist.tags"></tags-list>
<div class="ui hidden divider"></div>
</template>
<div class="header-buttons">
<div class="ui icon buttons">
<router-link class="ui labeled icon button" :to="{name: 'channels.detail', params: {id: object.uuid }}">
<i class="info icon"></i>
<translate translate-context="Content/Moderation/Link/Verb">Open local profile</translate>&nbsp;
</router-link>
<div class="ui floating dropdown icon button" v-dropdown>
<i class="dropdown icon"></i>
<div class="menu">
<a
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
class="basic item"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/audio/channel/${object.id}`)"
target="_blank" rel="noopener noreferrer">
<i class="wrench icon"></i>
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>&nbsp;
</a>
<fetch-button @refresh="fetchData" v-if="!object.actor.is_local" class="basic item" :url="`channels/${object.uuid}/fetches/`">
<i class="refresh icon"></i>&nbsp;
<translate translate-context="Content/Moderation/Button/Verb">Refresh from remote server</translate>&nbsp;
</fetch-button>
<a class="basic item" :href="object.actor.url || object.actor.fid" target="_blank" rel="noopener noreferrer">
<i class="external icon"></i>
<translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate>&nbsp;
</a>
</div>
</div>
</div>
<div class="ui buttons">
<dangerous-button
:class="['ui', {loading: isLoading}, 'basic red button']"
:action="remove">
<translate translate-context="*/*/*/Verb">Delete</translate>
<p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this channel?</translate></p>
<div slot="modal-content">
<p><translate translate-context="Content/Moderation/Paragraph">The channel will be removed, as well as associated uploads, tracks, and albums. This action is irreversible.</translate></p>
</div>
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
</dangerous-button>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="ui vertical stripe segment">
<div class="ui stackable three column grid">
<div class="column">
<section>
<h3 class="ui header">
<i class="info icon"></i>
<div class="content">
<translate translate-context="Content/Moderation/Title">Channel data</translate>
</div>
</h3>
<table class="ui very basic table">
<tbody>
<tr>
<td>
<translate translate-context="*/*/*/Noun">Name</translate>
</td>
<td>
{{ object.artist.name }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.channels', query: {q: getQuery('category', object.artist.content_category) }}">
<translate translate-context="*/*/*">Category</translate>
</router-link>
</td>
<td>
{{ object.artist.content_category }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.attributed_to.full_username }}">
<translate translate-context="*/*/*/Noun">Account</translate>
</router-link>
</td>
<td>
{{ object.attributed_to.preferred_username }}
</td>
</tr>
<tr v-if="!object.actor.is_local">
<td>
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.actor.domain }}">
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
</router-link>
</td>
<td>
{{ object.actor.domain }}
</td>
</tr>
<tr v-if="object.artist.description">
<td>
<translate translate-context="'*/*/*/Noun">Description</translate>
</td>
<td v-html="object.artist.description.html"></td>
</tr>
<tr v-if="object.actor.url">
<td>
<translate translate-context="'Content/*/*/Noun">URL</translate>
</td>
<td>
<a :href="object.actor.url" rel="noreferrer noopener" target="_blank">{{ object.actor.url }}</a>
</td>
</tr>
<tr v-if="object.rss_url">
<td>
<translate translate-context="'*/*/*">RSS Feed</translate>
</td>
<td>
<a :href="object.rss_url" rel="noreferrer noopener" target="_blank">{{ object.rss_url }}</a>
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="column">
<section>
<h3 class="ui header">
<i class="feed icon"></i>
<div class="content">
<translate translate-context="Content/Moderation/Title">Activity</translate>&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
</div>
</h3>
<div v-if="isLoadingStats" class="ui placeholder">
<div class="full line"></div>
<div class="short line"></div>
<div class="medium line"></div>
<div class="long line"></div>
</div>
<table v-else class="ui very basic table">
<tbody>
<tr>
<td>
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
</td>
<td>
<human-date :date="object.creation_date"></human-date>
</td>
</tr>
<tr>
<td>
<translate translate-context="*/*/*/Noun">Listenings</translate>
</td>
<td>
{{ stats.listenings }}
</td>
</tr>
<tr>
<td>
<translate translate-context="*/*/*">Favorited tracks</translate>
</td>
<td>
{{ stats.track_favorites }}
</td>
</tr>
<tr>
<td>
<translate translate-context="*/*/*">Playlists</translate>
</td>
<td>
{{ stats.playlists }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `channel:${object.uuid}`) }}">
<translate translate-context="Content/Moderation/Table.Label/Noun">Linked reports</translate>
</router-link>
</td>
<td>
{{ stats.reports }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.edits', query: {q: getQuery('target', 'artist ' + object.artist.id)}}">
<translate translate-context="*/Admin/*/Noun">Edits</translate>
</router-link>
</td>
<td>
{{ stats.mutations }}
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="column">
<section>
<h3 class="ui header">
<i class="music icon"></i>
<div class="content">
<translate translate-context="Content/Moderation/Title">Audio content</translate>&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
</div>
</h3>
<div v-if="isLoadingStats" class="ui placeholder">
<div class="full line"></div>
<div class="short line"></div>
<div class="medium line"></div>
<div class="long line"></div>
</div>
<table v-else class="ui very basic table">
<tbody>
<tr>
<td>
<translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate>
</td>
<td>
{{ stats.media_downloaded_size | humanSize }}
</td>
</tr>
<tr>
<td>
<translate translate-context="Content/Moderation/Table.Label">Total size</translate>
</td>
<td>
{{ stats.media_total_size | humanSize }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('channel_id', object.uuid) }}">
<translate translate-context="*/*/*">Uploads</translate>
</router-link>
</td>
<td>
{{ stats.uploads }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.albums', query: {q: getQuery('channel_id', object.uuid) }}">
<translate translate-context="*/*/*">Albums</translate>
</router-link>
</td>
<td>
{{ object.artist.albums_count }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('channel_id', object.uuid) }}">
<translate translate-context="*/*/*">Tracks</translate>
</router-link>
</td>
<td>
{{ object.artist.tracks_count }}
</td>
</tr>
</tbody>
</table>
</section>
</div>
</div>
</div>
</template>
</main>
</template>
<script>
import axios from "axios"
import logger from "@/logging"
import TagsList from "@/components/tags/List"
import FetchButton from "@/components/federation/FetchButton"
export default {
props: ["id"],
components: {
FetchButton,
TagsList
},
data() {
return {
isLoading: true,
isLoadingStats: false,
object: null,
stats: null,
}
},
created() {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData() {
var self = this
this.isLoading = true
let url = `manage/channels/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
},
fetchStats() {
var self = this
this.isLoadingStats = true
let url = `manage/channels/${this.id}/stats/`
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
remove () {
var self = this
this.isLoading = true
let url = `manage/channels/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({name: 'manage.channels'})
})
},
getQuery (field, value) {
return `${field}:"${value}"`
}
},
computed: {
labels() {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'),
}
},
}
}
</script>

Wyświetl plik

@ -0,0 +1,29 @@
<template>
<main v-title="labels.title">
<section class="ui vertical stripe segment">
<h2 class="ui header">{{ labels.title }}</h2>
<div class="ui hidden divider"></div>
<channels-table :update-url="true" :default-query="defaultQuery"></channels-table>
</section>
</main>
</template>
<script>
import ChannelsTable from "@/components/manage/ChannelsTable"
export default {
components: {
ChannelsTable
},
props: {
defaultQuery: {type: String, required: false},
},
computed: {
labels() {
return {
title: this.$pgettext('*/*/*', 'Channels')
}
}
}
}
</script>

Wyświetl plik

@ -108,6 +108,16 @@
{{ object.name }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.artists', query: {q: getQuery('category', object.content_category) }}">
<translate translate-context="*/*/*">Category</translate>
</router-link>
</td>
<td>
{{ object.content_category }}
</td>
</tr>
<tr v-if="!object.is_local">
<td>
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
@ -265,7 +275,7 @@
</router-link>
</td>
<td>
{{ object.albums.length }}
{{ object.albums_count }}
</td>
</tr>
<tr>
@ -275,7 +285,7 @@
</router-link>
</td>
<td>
{{ object.tracks.length }}
{{ object.tracks_count }}
</td>
</tr>
</tbody>
@ -321,8 +331,12 @@ export default {
this.isLoading = true
let url = `manage/library/artists/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
if (response.data.channel) {
self.$router.push({name: "manage.channels.detail", params: {id: response.data.channel}})
} else {
self.object = response.data
self.isLoading = false
}
})
},
fetchStats() {

Wyświetl plik

@ -4,6 +4,9 @@
<router-link
class="ui item"
:to="{name: 'manage.library.edits'}"><translate translate-context="*/Admin/*/Noun">Edits</translate></router-link>
<router-link
class="ui item"
:to="{name: 'manage.channels'}"><translate translate-context="*/*/*">Channels</translate></router-link>
<router-link
class="ui item"
:to="{name: 'manage.library.artists'}"><translate translate-context="*/*/*/Noun">Artists</translate></router-link>

Wyświetl plik

@ -343,7 +343,16 @@
{{ stats.media_total_size | humanSize }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.channels', query: {q: getQuery('account', object.full_username) }}">
<translate translate-context="*/*/*">Channels</translate>
</router-link>
</td>
<td>
{{ stats.channels }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('account', object.full_username) }}">

Wyświetl plik

@ -266,6 +266,16 @@
{{ stats.media_total_size | humanSize }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.channels', query: {q: getQuery('domain', object.name) }}">
<translate translate-context="*/*/*">Channels</translate>
</router-link>
</td>
<td>
{{ stats.channels }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">

Wyświetl plik

@ -84,7 +84,7 @@
<div
role="button"
class="basic item"
v-for="obj in getReportableObjs({channel: object})"
v-for="obj in getReportableObjs({account: object.attributed_to, channel: object})"
:key="obj.target.type + obj.target.id"
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
<i class="share icon" /> {{ obj.label }}
@ -112,7 +112,7 @@
</template>
<template v-if="$store.state.auth.availablePermissions['library']" >
<div class="divider"></div>
<router-link class="basic item" :to="{name: 'manage.library.channels.detail', params: {id: object.uuid}}">
<router-link class="basic item" :to="{name: 'manage.channels.detail', params: {id: object.uuid}}">
<i class="wrench icon"></i>
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
</router-link>