Edits for artists and albums

merge-requests/757/head
Eliot Berriot 2019-04-17 16:11:24 +02:00
rodzic 2836b11190
commit 55d0e52c55
15 zmienionych plików z 523 dodań i 151 usunięć

Wyświetl plik

@ -4,6 +4,7 @@ from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
from django.db.models import Lookup
from django.db.models.fields import Field
@ -70,8 +71,8 @@ class Mutation(models.Model):
applied_date = models.DateTimeField(null=True, blank=True, db_index=True)
summary = models.TextField(max_length=2000, null=True, blank=True)
payload = JSONField()
previous_state = JSONField(null=True, default=None)
payload = JSONField(encoder=DjangoJSONEncoder)
previous_state = JSONField(null=True, default=None, encoder=DjangoJSONEncoder)
target_id = models.IntegerField(null=True)
target_content_type = models.ForeignKey(

Wyświetl plik

@ -346,3 +346,37 @@ def outbox_update_track(context):
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}
@outbox.register({"type": "Update", "object.type": "Album"})
def outbox_update_album(context):
album = context["album"]
serializer = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.AlbumSerializer(album).data}
)
yield {
"type": "Update",
"actor": actors.get_service_actor(),
"payload": with_recipients(
serializer.data,
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}
@outbox.register({"type": "Update", "object.type": "Artist"})
def outbox_update_artist(context):
artist = context["artist"]
serializer = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.ArtistSerializer(artist).data}
)
yield {
"type": "Update",
"actor": actors.get_service_actor(),
"payload": with_recipients(
serializer.data,
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}

Wyświetl plik

@ -28,3 +28,35 @@ class TrackMutationSerializer(mutations.UpdateMutationSerializer):
routes.outbox.dispatch(
{"type": "Update", "object": {"type": "Track"}}, context={"track": obj}
)
@mutations.registry.connect(
"update",
models.Artist,
perm_checkers={"suggest": can_suggest, "approve": can_approve},
)
class ArtistMutationSerializer(mutations.UpdateMutationSerializer):
class Meta:
model = models.Artist
fields = ["name"]
def post_apply(self, obj, validated_data):
routes.outbox.dispatch(
{"type": "Update", "object": {"type": "Artist"}}, context={"artist": obj}
)
@mutations.registry.connect(
"update",
models.Album,
perm_checkers={"suggest": can_suggest, "approve": can_approve},
)
class AlbumMutationSerializer(mutations.UpdateMutationSerializer):
class Meta:
model = models.Album
fields = ["title", "release_date"]
def post_apply(self, obj, validated_data):
routes.outbox.dispatch(
{"type": "Update", "object": {"type": "Album"}}, context={"album": obj}
)

Wyświetl plik

@ -70,6 +70,8 @@ class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelV
filterset_class = filters.ArtistFilter
ordering_fields = ("id", "name", "creation_date")
mutations = common_decorators.mutations_route(types=["update"])
def get_queryset(self):
queryset = super().get_queryset()
albums = models.Album.objects.with_tracks_count()
@ -98,6 +100,8 @@ class AlbumViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelVi
ordering_fields = ("creation_date", "release_date", "title")
filterset_class = filters.AlbumFilter
mutations = common_decorators.mutations_route(types=["update"])
def get_queryset(self):
queryset = super().get_queryset()
tracks = (

Wyświetl plik

@ -448,6 +448,19 @@ def test_inbox_update_artist(factories, mocker):
update_library_entity.assert_called_once_with(obj, {"name": "New name"})
def test_outbox_update_artist(factories):
artist = factories["music.Artist"]()
activity = list(routes.outbox_update_artist({"artist": artist}))[0]
expected = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.ArtistSerializer(artist).data}
).data
expected["to"] = [contexts.AS.Public, {"type": "instances_with_followers"}]
assert dict(activity["payload"]) == dict(expected)
assert activity["actor"] == actors.get_service_actor()
def test_inbox_update_album(factories, mocker):
update_library_entity = mocker.patch(
"funkwhale_api.music.tasks.update_library_entity"
@ -466,6 +479,19 @@ def test_inbox_update_album(factories, mocker):
update_library_entity.assert_called_once_with(obj, {"title": "New title"})
def test_outbox_update_album(factories):
album = factories["music.Album"]()
activity = list(routes.outbox_update_album({"album": album}))[0]
expected = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.AlbumSerializer(album).data}
).data
expected["to"] = [contexts.AS.Public, {"type": "instances_with_followers"}]
assert dict(activity["payload"]) == dict(expected)
assert activity["actor"] == actors.get_service_actor()
def test_inbox_update_track(factories, mocker):
update_library_entity = mocker.patch(
"funkwhale_api.music.tasks.update_library_entity"

Wyświetl plik

@ -1,6 +1,54 @@
import datetime
import pytest
from funkwhale_api.music import licenses
@pytest.mark.parametrize(
"field, old_value, new_value, expected", [("name", "foo", "bar", "bar")]
)
def test_artist_mutation(field, old_value, new_value, expected, factories, now, mocker):
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
artist = factories["music.Artist"](**{field: old_value})
mutation = factories["common.Mutation"](
type="update", target=artist, payload={field: new_value}
)
mutation.apply()
artist.refresh_from_db()
assert getattr(artist, field) == expected
dispatch.assert_called_once_with(
{"type": "Update", "object": {"type": "Artist"}}, context={"artist": artist}
)
@pytest.mark.parametrize(
"field, old_value, new_value, expected",
[
("title", "foo", "bar", "bar"),
(
"release_date",
datetime.date(2016, 1, 1),
"2018-02-01",
datetime.date(2018, 2, 1),
),
],
)
def test_album_mutation(field, old_value, new_value, expected, factories, now, mocker):
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
album = factories["music.Album"](**{field: old_value})
mutation = factories["common.Mutation"](
type="update", target=album, payload={field: new_value}
)
mutation.apply()
album.refresh_from_db()
assert getattr(album, field) == expected
dispatch.assert_called_once_with(
{"type": "Update", "object": {"type": "Album"}}, context={"album": album}
)
def test_track_license_mutation(factories, now):
track = factories["music.Track"](license=None)
mutation = factories["common.Mutation"](

Wyświetl plik

@ -1,15 +1,15 @@
<template>
<main>
<div v-if="isLoading" class="ui vertical segment" v-title="">
<div v-if="isLoading" class="ui vertical segment" v-title="labels.title">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<template v-if="album">
<section :class="['ui', 'head', {'with-background': album.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="album.title">
<template v-if="object">
<section :class="['ui', 'head', {'with-background': object.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="object.title">
<div class="segment-content">
<h2 class="ui center aligned icon header">
<i class="circular inverted sound yellow icon"></i>
<div class="content">
{{ album.title }}
{{ object.title }}
<div v-html="subtitle"></div>
</div>
</h2>
@ -17,7 +17,7 @@
<div class="header-buttons">
<div class="ui buttons">
<play-button class="orange" :tracks="album.tracks">
<play-button class="orange" :tracks="object.tracks">
<translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
</play-button>
</div>
@ -28,7 +28,7 @@
</div>
<div class="content">
<div class="description">
<embed-wizard type="album" :id="album.id" />
<embed-wizard type="album" :id="object.id" />
</div>
</div>
@ -61,15 +61,22 @@
<i class="external icon"></i>
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
</a>
<router-link
v-if="object.is_local"
:to="{name: 'library.albums.edit', params: {id: object.id }}"
class="basic item">
<i class="edit icon"></i>
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
</router-link>
<div class="divider"></div>
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: album.id}}">
<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>
</router-link>
<a
v-if="$store.state.auth.profile.is_superuser"
class="basic item"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${album.id}`)"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${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;
@ -80,36 +87,7 @@
</div>
</div>
</section>
<template v-if="discs && discs.length > 1">
<section v-for="(tracks, disc_number) in discs" class="ui vertical stripe segment">
<translate
tag="h2"
class="left floated"
:translate-params="{number: disc_number + 1}"
translate-context="Content/Album/"
>Volume %{ number }</translate>
<play-button class="right floated orange" :tracks="tracks">
<translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
</play-button>
<track-table :artist="album.artist" :display-position="true" :tracks="tracks"></track-table>
</section>
</template>
<template v-else>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="*/*/*/Noun">Tracks</translate>
</h2>
<track-table v-if="album" :artist="album.artist" :display-position="true" :tracks="album.tracks"></track-table>
</section>
</template>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
</h2>
<library-widget @loaded="libraries = $event" :url="'albums/' + id + '/libraries/'">
<translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate>
</library-widget>
</section>
<router-view v-if="object" :discs="discs" @libraries-loaded="libraries = $event" :object="object" object-type="album" :key="$route.fullPath"></router-view>
</template>
</main>
</template>
@ -119,13 +97,12 @@ import axios from "axios"
import logger from "@/logging"
import backend from "@/audio/backend"
import PlayButton from "@/components/audio/PlayButton"
import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from "@/components/federation/LibraryWidget"
import EmbedWizard from "@/components/audio/EmbedWizard"
import Modal from '@/components/semantic/Modal'
const FETCH_URL = "albums/"
function groupByDisc(acc, track) {
var dn = track.disc_number - 1
if (dn < 0) dn = 0
@ -141,15 +118,13 @@ export default {
props: ["id"],
components: {
PlayButton,
TrackTable,
LibraryWidget,
EmbedWizard,
Modal
},
data() {
return {
isLoading: true,
album: null,
object: null,
discs: [],
libraries: [],
showEmbedModal: false
@ -165,8 +140,8 @@ export default {
let url = FETCH_URL + this.id + "/"
logger.default.debug('Fetching album "' + this.id + '"')
axios.get(url).then(response => {
self.album = backend.Album.clean(response.data)
self.discs = self.album.tracks.reduce(groupByDisc, [])
self.object = backend.Album.clean(response.data)
self.discs = self.object.tracks.reduce(groupByDisc, [])
self.isLoading = false
})
}
@ -185,28 +160,28 @@ export default {
wikipediaUrl() {
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.album.title + " " + this.album.artist.name)
encodeURI(this.object.title + " " + this.object.artist.name)
)
},
musicbrainzUrl() {
if (this.album.mbid) {
return "https://musicbrainz.org/release/" + this.album.mbid
if (this.object.mbid) {
return "https://musicbrainz.org/release/" + this.object.mbid
}
},
headerStyle() {
if (!this.album.cover.original) {
if (!this.object.cover.original) {
return ""
}
return (
"background-image: url(" +
this.$store.getters["instance/absoluteUrl"](this.album.cover.original) +
this.$store.getters["instance/absoluteUrl"](this.object.cover.original) +
")"
)
},
subtitle () {
let route = this.$router.resolve({name: 'library.artists.detail', params: {id: this.album.artist.id }})
let msg = this.$npgettext('Content/Album/Header.Title', 'Album containing %{ count } track, by <a class="internal" href="%{ artistUrl }">%{ artist }</a>', 'Album containing %{ count } tracks, by <a class="internal" href="%{ artistUrl }">%{ artist }</a>', this.album.tracks.length)
return this.$gettextInterpolate(msg, {count: this.album.tracks.length, artist: this.album.artist.name, artistUrl: route.location.path})
let route = this.$router.resolve({name: 'library.artists.detail', params: {id: this.object.artist.id }})
let msg = this.$npgettext('Content/Album/Header.Title', 'Album containing %{ count } track, by <a class="internal" href="%{ artistUrl }">%{ artist }</a>', 'Album containing %{ count } tracks, by <a class="internal" href="%{ artistUrl }">%{ artist }</a>', this.object.tracks.length)
return this.$gettextInterpolate(msg, {count: this.object.tracks.length, artist: this.object.artist.name, artistUrl: route.location.path})
}
},
watch: {
@ -216,7 +191,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>

Wyświetl plik

@ -0,0 +1,62 @@
<template>
<div v-if="object">
<template v-if="discs && discs.length > 1">
<section v-for="(tracks, disc_number) in discs" class="ui vertical stripe segment">
<translate
tag="h2"
class="left floated"
:translate-params="{number: disc_number + 1}"
translate-context="Content/Album/"
>Volume %{ number }</translate>
<play-button class="right floated orange" :tracks="tracks">
<translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
</play-button>
<track-table :artist="object.artist" :display-position="true" :tracks="tracks"></track-table>
</section>
</template>
<template v-else>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="*/*/*/Noun">Tracks</translate>
</h2>
<track-table v-if="object" :artist="object.artist" :display-position="true" :tracks="object.tracks"></track-table>
</section>
</template>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
</h2>
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'albums/' + object.id + '/libraries/'">
<translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate>
</library-widget>
</section>
</div>
</template>
<script>
import time from "@/utils/time"
import axios from "axios"
import url from "@/utils/url"
import logger from "@/logging"
import LibraryWidget from "@/components/federation/LibraryWidget"
import TrackTable from "@/components/audio/track/Table"
export default {
props: ["object", "libraries", "discs"],
components: {
LibraryWidget,
TrackTable
},
data() {
return {
time,
id: this.object.id,
}
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
</style>

Wyświetl plik

@ -0,0 +1,41 @@
<template>
<section class="ui vertical stripe segment">
<div class="ui text container">
<h2>
<translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this album</translate>
<translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this album</translate>
</h2>
<div class="ui message" v-if="!object.is_local">
<translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate>
</div>
<edit-form
v-else
:object-type="objectType"
:object="object"
:can-edit="canEdit"></edit-form>
</div>
</section>
</template>
<script>
import axios from "axios"
import EditForm from '@/components/library/EditForm'
export default {
props: ["objectType", "object", "libraries"],
data() {
return {
id: this.object.id,
}
},
components: {
EditForm
},
computed: {
canEdit () {
return true
}
}
}
</script>

Wyświetl plik

@ -3,13 +3,13 @@
<div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<template v-if="artist">
<section :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="artist.name">
<template v-if="object">
<section :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="object.name">
<div class="segment-content">
<h2 class="ui center aligned icon header">
<i class="circular inverted users violet icon"></i>
<div class="content">
{{ artist.name }}
{{ object.name }}
<div class="sub header" v-if="albums">
<translate translate-context="Content/Artist/Paragraph"
tag="div"
@ -24,11 +24,11 @@
<div class="ui hidden divider"></div>
<div class="header-buttons">
<div class="ui buttons">
<radio-button type="artist" :object-id="artist.id"></radio-button>
<radio-button type="artist" :object-id="object.id"></radio-button>
</div>
<div class="ui buttons">
<play-button :is-playable="isPlayable" class="orange" :artist="artist">
<play-button :is-playable="isPlayable" class="orange" :artist="object">
<translate translate-context="Content/Artist/Button.Label/Verb">Play all albums</translate>
</play-button>
</div>
@ -39,7 +39,7 @@
</div>
<div class="content">
<div class="description">
<embed-wizard type="artist" :id="artist.id" />
<embed-wizard type="artist" :id="object.id" />
</div>
</div>
@ -72,15 +72,22 @@
<i class="external icon"></i>
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
</a>
<router-link
v-if="object.is_local"
:to="{name: 'library.artists.edit', params: {id: object.id }}"
class="basic item">
<i class="edit icon"></i>
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
</router-link>
<div class="divider"></div>
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.artists.detail', params: {id: artist.id}}">
<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>
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
</router-link>
<a
v-if="$store.state.auth.profile.is_superuser"
class="basic item"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${artist.id}`)"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${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;
@ -91,84 +98,40 @@
</div>
</div>
</section>
<div class="ui small text container" v-if="contentFilter">
<div class="ui hidden divider"></div>
<div class="ui message">
<p>
<translate translate-context="Content/Artist/Paragraph">You are currently hiding content related to this artist.</translate>
</p>
<router-link class="right floated" :to="{name: 'settings'}">
<translate translate-context="Content/Moderation/Link">Review my filters</translate>
</router-link>
<button @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" class="ui basic tiny button">
<translate translate-context="Content/Moderation/Button.Label">Remove filter</translate>
</button>
</div>
</div>
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</section>
<section v-else-if="albums && albums.length > 0" class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/Artist/Title">Albums by this artist</translate>
</h2>
<div class="ui cards" >
<album-card :mode="'rich'" :album="album" :key="album.id" v-for="album in albums"></album-card>
</div>
</section>
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/Artist/Title">Tracks by this artist</translate>
</h2>
<track-table :display-position="true" :tracks="tracks"></track-table>
</section>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
</h2>
<library-widget @loaded="libraries = $event" :url="'artists/' + id + '/libraries/'">
<translate translate-context="Content/Artist/Paragraph" slot="subtitle">This artist is present in the following libraries:</translate>
</library-widget>
</section>
<router-view v-if="object" :tracks="tracks" :albums="albums" :is-loading-albums="isLoadingAlbums" @libraries-loaded="libraries = $event" :object="object" object-type="artist" :key="$route.fullPath"></router-view>
</template>
</main>
</template>
<script>
import _ from "@/lodash"
import axios from "axios"
import logger from "@/logging"
import backend from "@/audio/backend"
import AlbumCard from "@/components/audio/album/Card"
import RadioButton from "@/components/radios/Button"
import PlayButton from "@/components/audio/PlayButton"
import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from "@/components/federation/LibraryWidget"
import EmbedWizard from "@/components/audio/EmbedWizard"
import Modal from '@/components/semantic/Modal'
import RadioButton from "@/components/radios/Button"
const FETCH_URL = "albums/"
export default {
props: ["id"],
components: {
AlbumCard,
RadioButton,
PlayButton,
TrackTable,
LibraryWidget,
EmbedWizard,
Modal
Modal,
RadioButton
},
data() {
return {
isLoading: true,
isLoadingAlbums: true,
artist: null,
object: null,
albums: null,
totalTracks: 0,
totalAlbums: 0,
tracks: [],
libraries: [],
showEmbedModal: false
showEmbedModal: false,
tracks: [],
}
},
created() {
@ -184,7 +147,7 @@ export default {
self.totalTracks = response.data.count
})
axios.get("artists/" + this.id + "/").then(response => {
self.artist = response.data
self.object = response.data
self.isLoading = false
self.isLoadingAlbums = true
axios
@ -204,40 +167,31 @@ export default {
}
},
computed: {
labels() {
return {
title: this.$pgettext('*/*/*/Noun', "Artist")
}
},
isPlayable() {
return (
this.artist.albums.filter(a => {
this.object.albums.filter(a => {
return a.is_playable
}).length > 0
)
},
labels() {
return {
title: this.$pgettext('*/*/*', 'Album')
}
},
wikipediaUrl() {
return (
"https://en.wikipedia.org/w/index.php?search=" +
encodeURI(this.artist.name)
encodeURI(this.object.name)
)
},
musicbrainzUrl() {
if (this.artist.mbid) {
return "https://musicbrainz.org/artist/" + this.artist.mbid
if (this.object.mbid) {
return "https://musicbrainz.org/artist/" + this.object.mbid
}
},
allTracks() {
let tracks = []
this.albums.forEach(album => {
album.tracks.forEach(track => {
tracks.push(track)
})
})
return tracks
},
cover() {
return this.artist.albums
return this.object.albums
.filter(album => {
return album.cover
})
@ -264,7 +218,7 @@ export default {
contentFilter () {
let self = this
return this.$store.getters['moderation/artistFilters']().filter((e) => {
return e.target.id === this.artist.id
return e.target.id === this.object.id
})[0]
}
},
@ -275,7 +229,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

Wyświetl plik

@ -0,0 +1,79 @@
<template>
<div v-if="object">
<div class="ui small text container" v-if="contentFilter">
<div class="ui hidden divider"></div>
<div class="ui message">
<p>
<translate translate-context="Content/Artist/Paragraph">You are currently hiding content related to this artist.</translate>
</p>
<router-link class="right floated" :to="{name: 'settings'}">
<translate translate-context="Content/Moderation/Link">Review my filters</translate>
</router-link>
<button @click="$store.dispatch('moderation/deleteContentFilter', contentFilter.uuid)" class="ui basic tiny button">
<translate translate-context="Content/Moderation/Button.Label">Remove filter</translate>
</button>
</div>
</div>
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</section>
<section v-else-if="albums && albums.length > 0" class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/Artist/Title">Albums by this artist</translate>
</h2>
<div class="ui cards" >
<album-card :mode="'rich'" :album="album" :key="album.id" v-for="album in albums"></album-card>
</div>
</section>
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/Artist/Title">Tracks by this artist</translate>
</h2>
<track-table :display-position="true" :tracks="tracks"></track-table>
</section>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
</h2>
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'artists/' + object.id + '/libraries/'">
<translate translate-context="Content/Artist/Paragraph" slot="subtitle">This artist is present in the following libraries:</translate>
</library-widget>
</section>
</div>
</template>
<script>
import _ from "@/lodash"
import axios from "axios"
import logger from "@/logging"
import backend from "@/audio/backend"
import AlbumCard from "@/components/audio/album/Card"
import TrackTable from "@/components/audio/track/Table"
import LibraryWidget from "@/components/federation/LibraryWidget"
export default {
props: ["object", "tracks", "albums", "isLoadingAlbums"],
components: {
AlbumCard,
TrackTable,
LibraryWidget,
},
computed: {
contentFilter () {
let self = this
return this.$store.getters['moderation/artistFilters']().filter((e) => {
return e.target.id === this.object.id
})[0]
}
},
watch: {
id() {
this.fetchData()
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

Wyświetl plik

@ -0,0 +1,41 @@
<template>
<section class="ui vertical stripe segment">
<div class="ui text container">
<h2>
<translate v-if="canEdit" key="1" translate-context="Content/*/Title">Edit this artist</translate>
<translate v-else key="2" translate-context="Content/*/Title">Suggest an edit on this artist</translate>
</h2>
<div class="ui message" v-if="!object.is_local">
<translate translate-context="Content/*/Message">This object is managed by another server, you cannot edit it.</translate>
</div>
<edit-form
v-else
:object-type="objectType"
:object="object"
:can-edit="canEdit"></edit-form>
</div>
</section>
</template>
<script>
import axios from "axios"
import EditForm from '@/components/library/EditForm'
export default {
props: ["objectType", "object", "libraries"],
data() {
return {
id: this.object.id,
}
},
components: {
EditForm
},
computed: {
canEdit () {
return true
}
}
}
</script>

Wyświetl plik

@ -149,6 +149,12 @@ export default {
if (this.objectType === 'track') {
return `tracks/${this.object.id}/mutations/`
}
if (this.objectType === 'album') {
return `albums/${this.object.id}/mutations/`
}
if (this.objectType === 'artist') {
return `artists/${this.object.id}/mutations/`
}
},
mutationPayload () {
let self = this

Wyświetl plik

@ -1,13 +1,42 @@
export default {
getConfigs () {
return {
artist: {
fields: [
{
id: 'name',
type: 'text',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
]
},
album: {
fields: [
{
id: 'title',
type: 'text',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title }
},
{
id: 'release_date',
type: 'text',
required: false,
label: this.$pgettext('Content/*/*/Noun', 'Release date'),
getValue: (obj) => { return obj.release_date }
},
]
},
track: {
fields: [
{
id: 'title',
type: 'text',
required: true,
label: this.$pgettext('Content/Track/*/Noun', 'Title'),
label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title }
},
{

Wyświetl plik

@ -16,10 +16,14 @@ import PasswordResetConfirm from '@/views/auth/PasswordResetConfirm'
import EmailConfirm from '@/views/auth/EmailConfirm'
import Library from '@/components/library/Library'
import LibraryHome from '@/components/library/Home'
import LibraryArtist from '@/components/library/Artist'
import LibraryArtists from '@/components/library/Artists'
import LibraryArtistDetail from '@/components/library/ArtistDetail'
import LibraryArtistEdit from '@/components/library/ArtistEdit'
import LibraryArtistDetailBase from '@/components/library/ArtistBase'
import LibraryAlbums from '@/components/library/Albums'
import LibraryAlbum from '@/components/library/Album'
import LibraryAlbumDetail from '@/components/library/AlbumDetail'
import LibraryAlbumEdit from '@/components/library/AlbumEdit'
import LibraryAlbumDetailBase from '@/components/library/AlbumBase'
import LibraryTrackDetail from '@/components/library/TrackDetail'
import LibraryTrackEdit from '@/components/library/TrackEdit'
import EditDetail from '@/components/library/EditDetail'
@ -411,8 +415,52 @@ export default new Router({
id: route.params.id,
defaultEdit: route.query.mode === 'edit' })
},
{ path: 'artists/:id', name: 'library.artists.detail', component: LibraryArtist, props: true },
{ path: 'albums/:id', name: 'library.albums.detail', component: LibraryAlbum, props: true },
{
path: 'artists/:id',
component: LibraryArtistDetailBase,
props: true,
children: [
{
path: '',
name: 'library.artists.detail',
component: LibraryArtistDetail
},
{
path: 'edit',
name: 'library.artists.edit',
component: LibraryArtistEdit
},
{
path: 'edit/:editId',
name: 'library.artists.edit.detail',
component: EditDetail,
props: true,
}
]
},
{
path: 'albums/:id',
component: LibraryAlbumDetailBase,
props: true,
children: [
{
path: '',
name: 'library.albums.detail',
component: LibraryAlbumDetail
},
{
path: 'edit',
name: 'library.albums.edit',
component: LibraryAlbumEdit
},
{
path: 'edit/:editId',
name: 'library.albums.edit.detail',
component: EditDetail,
props: true,
}
]
},
{
path: 'tracks/:id',
component: LibraryTrackDetailBase,