Merge branch '853-ui-allow-list' into 'develop'

UI for allow-list

See merge request funkwhale/funkwhale!800
This commit is contained in:
Eliot Berriot 2019-06-26 10:22:53 +02:00
commit b9302200d1
9 changed files with 175 additions and 16 deletions

View File

@ -235,12 +235,23 @@ class ManageUploadFilterSet(filters.FilterSet):
] ]
def filter_allowed(queryset, name, value):
"""
If value=false, we want to include object with value=null as well
"""
if value:
return queryset.filter(allowed=True)
else:
return queryset.filter(Q(allowed=False) | Q(allowed__isnull=True))
class ManageDomainFilterSet(filters.FilterSet): class ManageDomainFilterSet(filters.FilterSet):
q = fields.SearchFilter(search_fields=["name"]) q = fields.SearchFilter(search_fields=["name"])
allowed = filters.BooleanFilter(method=filter_allowed)
class Meta: class Meta:
model = federation_models.Domain model = federation_models.Domain
fields = ["name"] fields = ["name", "allowed"]
class ManageActorFilterSet(filters.FilterSet): class ManageActorFilterSet(filters.FilterSet):

View File

@ -152,7 +152,11 @@ class ManageDomainUpdateSerializer(ManageDomainSerializer):
class ManageDomainActionSerializer(common_serializers.ActionSerializer): class ManageDomainActionSerializer(common_serializers.ActionSerializer):
actions = [common_serializers.Action("purge", allow_all=False)] actions = [
common_serializers.Action("purge", allow_all=False),
common_serializers.Action("allow_list_add", allow_all=True),
common_serializers.Action("allow_list_remove", allow_all=True),
]
filterset_class = filters.ManageDomainFilterSet filterset_class = filters.ManageDomainFilterSet
pk_field = "name" pk_field = "name"
@ -161,6 +165,14 @@ class ManageDomainActionSerializer(common_serializers.ActionSerializer):
ids = objects.values_list("pk", flat=True) ids = objects.values_list("pk", flat=True)
common_utils.on_commit(federation_tasks.purge_actors.delay, domains=list(ids)) common_utils.on_commit(federation_tasks.purge_actors.delay, domains=list(ids))
@transaction.atomic
def handle_allow_list_add(self, objects):
objects.update(allowed=True)
@transaction.atomic
def handle_allow_list_remove(self, objects):
objects.update(allowed=False)
class ManageBaseActorSerializer(serializers.ModelSerializer): class ManageBaseActorSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -19,8 +19,8 @@ class AllowListPublic(types.BooleanPreference):
name = "allow_list_public" name = "allow_list_public"
verbose_name = "Publish your allowed-domains list" verbose_name = "Publish your allowed-domains list"
help_text = ( help_text = (
"If enabled, everyone will be able to retrieve the list of domains you allowed. ", "If enabled, everyone will be able to retrieve the list of domains you allowed. "
"This is useful on open setups, to help people decide if they want to join your pod, or to " "This is useful on open setups, to help people decide if they want to join your pod, or to "
"make your moderation policy public.", "make your moderation policy public."
) )
default = False default = False

View File

@ -176,6 +176,26 @@ def test_manage_domain_action_purge(factories, mocker):
) )
def test_manage_domain_action_allow_list_add(factories, mocker):
domains = factories["federation.Domain"].create_batch(size=3, allowed=False)
s = serializers.ManageDomainActionSerializer(queryset=None)
s.handle_allow_list_add(domains[0].__class__.objects.all())
for domain in domains:
domain.refresh_from_db()
assert domain.allowed is True
def test_manage_domain_action_allow_list_remove(factories, mocker):
domains = factories["federation.Domain"].create_batch(size=3, allowed=True)
s = serializers.ManageDomainActionSerializer(queryset=None)
s.handle_allow_list_remove(domains[0].__class__.objects.all())
for domain in domains:
domain.refresh_from_db()
assert domain.allowed is False
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param,expected_only", [("block_all", []), ("reject_media", ["media"])] "param,expected_only", [("block_all", []), ("reject_media", ["media"])]
) )

View File

@ -6,6 +6,14 @@
<label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label> <label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
<input name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" /> <input name="search" type="text" v-model="search" :placeholder="labels.searchPlaceholder" />
</div> </div>
<div class="field" v-if="allowListEnabled">
<label><translate translate-context="Content/Moderation/*/Adjective">Is present on allow-list</translate></label>
<select class="ui dropdown" v-model="allowed">
<option :value="null"><translate translate-context="*/*/*">All</translate></option>
<option :value="true"><translate translate-context="*/*/*">Yes</translate></option>
<option :value="false"><translate translate-context="*/*/*">No</translate></option>
</select>
</div>
<div class="field"> <div class="field">
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> <label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
<select class="ui dropdown" v-model="ordering"> <select class="ui dropdown" v-model="ordering">
@ -44,7 +52,10 @@
</template> </template>
<template slot="row-cells" slot-scope="scope"> <template slot="row-cells" slot-scope="scope">
<td> <td>
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.name }}">{{ scope.obj.name }}</router-link> <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.name }}">
{{ scope.obj.name }}
<i v-if="allowListEnabled && scope.obj.allowed" class="green check icon" :title="labels.allowListTitle"></i>
</router-link>
</td> </td>
<td> <td>
{{ scope.obj.actors_count }} {{ scope.obj.actors_count }}
@ -93,7 +104,8 @@ import TranslationsMixin from '@/components/mixins/Translations'
export default { export default {
mixins: [OrderingMixin, TranslationsMixin], mixins: [OrderingMixin, TranslationsMixin],
props: { props: {
filters: {type: Object, required: false} filters: {type: Object, required: false},
allowListEnabled: {type: Boolean, default: false},
}, },
components: { components: {
Pagination, Pagination,
@ -108,6 +120,7 @@ export default {
page: 1, page: 1,
paginateBy: 50, paginateBy: 50,
search: '', search: '',
allowed: null,
orderingDirection: defaultOrdering.direction || '+', orderingDirection: defaultOrdering.direction || '+',
ordering: defaultOrdering.field, ordering: defaultOrdering.field,
orderingOptions: [ orderingOptions: [
@ -124,12 +137,16 @@ export default {
}, },
methods: { methods: {
fetchData () { fetchData () {
let params = _.merge({ let baseFilters = {
'page': this.page, 'page': this.page,
'page_size': this.paginateBy, 'page_size': this.paginateBy,
'q': this.search, 'q': this.search,
'ordering': this.getOrderingAsString() 'ordering': this.getOrderingAsString(),
}, this.filters) }
if (this.allowed !== null) {
baseFilters.allowed = this.allowed
}
let params = _.merge(baseFilters, this.filters)
let self = this let self = this
self.isLoading = true self.isLoading = true
self.checked = [] self.checked = []
@ -148,7 +165,8 @@ export default {
computed: { computed: {
labels () { labels () {
return { return {
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by name…') searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by name…'),
allowListTitle: this.$pgettext('Content/Moderation/Popup', 'This domain is present in your allow-list'),
} }
}, },
actionFilters () { actionFilters () {
@ -167,7 +185,21 @@ export default {
name: 'purge', name: 'purge',
label: this.$pgettext('*/*/*/Verb', 'Purge'), label: this.$pgettext('*/*/*/Verb', 'Purge'),
isDangerous: true isDangerous: true
} },
{
name: 'allow_list_add',
label: this.$pgettext('Content/Moderation/Action/Verb', 'Add to allow-list'),
filterCheckable: (obj) => {
return !obj.allowed
}
},
{
name: 'allow_list_remove',
label: this.$pgettext('Content/Moderation/Action/Verb', 'Remove from allow-list'),
filterCheckable: (obj) => {
return obj.allowed
}
},
] ]
} }
}, },
@ -179,6 +211,9 @@ export default {
page () { page () {
this.fetchData() this.fetchData()
}, },
allowed () {
this.fetchData()
},
ordering () { ordering () {
this.fetchData() this.fetchData()
}, },

View File

@ -82,6 +82,7 @@ export default {
let musicLabel = this.$pgettext('*/*/*/Noun', 'Music') let musicLabel = this.$pgettext('*/*/*/Noun', 'Music')
let playlistsLabel = this.$pgettext('*/*/*', 'Playlists') let playlistsLabel = this.$pgettext('*/*/*', 'Playlists')
let federationLabel = this.$pgettext('Content/Admin/Menu', 'Federation') let federationLabel = this.$pgettext('Content/Admin/Menu', 'Federation')
let moderationLabel = this.$pgettext('Content/Admin/Menu', 'Moderation')
let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic') let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic')
let statisticsLabel = this.$pgettext('Content/Admin/Menu', 'Statistics') let statisticsLabel = this.$pgettext('Content/Admin/Menu', 'Statistics')
let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting') let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting')
@ -118,6 +119,14 @@ export default {
id: "playlists", id: "playlists",
settings: ["playlists__max_tracks"] settings: ["playlists__max_tracks"]
}, },
{
label: moderationLabel,
id: "moderation",
settings: [
"moderation__allow_list_enabled",
"moderation__allow_list_public",
]
},
{ {
label: federationLabel, label: federationLabel,
id: "federation", id: "federation",

View File

@ -9,12 +9,31 @@
:to="{name: 'manage.moderation.accounts.list'}"><translate translate-context="*/Moderation/Title">Accounts</translate></router-link> :to="{name: 'manage.moderation.accounts.list'}"><translate translate-context="*/Moderation/Title">Accounts</translate></router-link>
</nav> </nav>
<router-view :key="$route.fullPath"></router-view> <router-view :allow-list-enabled="allowListEnabled" :key="$route.fullPath"></router-view>
</div> </div>
</template> </template>
<script> <script>
import _ from '@/lodash'
import axios from 'axios'
export default { export default {
data () {
return {
allowListEnabled: false
}
},
created () {
this.fetchNodeInfo()
},
methods: {
fetchNodeInfo () {
let self = this
axios.get('instance/nodeinfo/2.0/').then(response => {
self.allowListEnabled = _.get(response.data, 'metadata.allowList.enabled', false)
})
},
},
computed: { computed: {
labels() { labels() {
return { return {
@ -22,6 +41,6 @@ export default {
secondaryMenu: this.$pgettext('Menu/*/Hidden text', "Secondary menu") secondaryMenu: this.$pgettext('Menu/*/Hidden text', "Secondary menu")
} }
} }
} },
} }
</script> </script>

View File

@ -20,6 +20,34 @@
</div> </div>
</div> </div>
</h2> </h2>
<div class="header-buttons">
<div class="ui icon buttons">
<a
v-if="$store.state.auth.profile.is_superuser"
class="ui labeled icon button"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/federation/domain/${object.name}`)"
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>
</div>
<div v-if="allowListEnabled" class="ui icon buttons">
<button
v-if="object.allowed"
@click.prevent="setAllowList(false)"
:class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']">
<i class="x icon"></i>
<translate translate-context="Content/Moderation/Link/Verb">Remove from allow-list</translate>
</button>
<button
v-else
@click.prevent="setAllowList(true)"
:class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']">
<i class="check icon"></i>
<translate translate-context="Content/Moderation/Link/Verb">Add to allow-list</translate>
</button>
</div>
</div>
</div> </div>
</div> </div>
<div class="ui column"> <div class="ui column">
@ -74,6 +102,15 @@
</h3> </h3>
<table class="ui very basic table"> <table class="ui very basic table">
<tbody> <tbody>
<tr v-if="allowListEnabled">
<td>
<translate translate-context="Content/Moderation/*/Adjective">Is present on allow-list</translate>
</td>
<td>
<translate v-if="object.allowed" translate-context="*/*/*">Yes</translate>
<translate v-else translate-context="*/*/*">No</translate>
</td>
</tr>
<tr> <tr>
<td> <td>
<translate translate-context="Content/*/Table.Label">Last checked</translate> <translate translate-context="Content/*/Table.Label">Last checked</translate>
@ -300,7 +337,7 @@ import InstancePolicyForm from "@/components/manage/moderation/InstancePolicyFor
import InstancePolicyCard from "@/components/manage/moderation/InstancePolicyCard" import InstancePolicyCard from "@/components/manage/moderation/InstancePolicyCard"
export default { export default {
props: ["id"], props: ["id", "allowListEnabled"],
components: { components: {
InstancePolicyForm, InstancePolicyForm,
InstancePolicyCard, InstancePolicyCard,
@ -311,6 +348,7 @@ export default {
isLoading: true, isLoading: true,
isLoadingStats: false, isLoadingStats: false,
isLoadingPolicy: false, isLoadingPolicy: false,
isLoadingAllowList: false,
policy: null, policy: null,
object: null, object: null,
stats: null, stats: null,
@ -353,6 +391,15 @@ export default {
self.isLoadingPolicy = false self.isLoadingPolicy = false
}) })
}, },
setAllowList(value) {
var self = this
this.isLoadingAllowList = true
let url = `manage/federation/domains/${this.id}/`
axios.patch(url, {allowed: value}).then(response => {
self.object = response.data
self.isLoadingAllowList = false
})
},
refreshNodeInfo (data) { refreshNodeInfo (data) {
this.object.nodeinfo = data this.object.nodeinfo = data
this.object.nodeinfo_fetch_date = new Date() this.object.nodeinfo_fetch_date = new Date()

View File

@ -14,6 +14,10 @@
<label for="domain"><translate translate-context="Content/Moderation/Form.Label/Verb">Add a domain</translate></label> <label for="domain"><translate translate-context="Content/Moderation/Form.Label/Verb">Add a domain</translate></label>
<input type="text" name="domain" id="domain" v-model="domainName"> <input type="text" name="domain" id="domain" v-model="domainName">
</div> </div>
<div class="field" v-if="allowListEnabled">
<input type="checkbox" name="allowed" id="allowed" v-model="domainAllowed">
<label for="allowed"><translate translate-context="Content/Moderation/Form.Label/Verb">Add to allow-list</translate></label>
</div>
<div class="field"> <div class="field">
<button :class="['ui', {'loading': isCreating}, 'green', 'button']" type="submit" :disabled="isCreating"> <button :class="['ui', {'loading': isCreating}, 'green', 'button']" type="submit" :disabled="isCreating">
<label for="domain"><translate translate-context="Content/Moderation/Button/Verb">Add</translate></label> <label for="domain"><translate translate-context="Content/Moderation/Button/Verb">Add</translate></label>
@ -22,7 +26,7 @@
</div> </div>
</form> </form>
<div class="ui clearing hidden divider"></div> <div class="ui clearing hidden divider"></div>
<domains-table></domains-table> <domains-table :allow-list-enabled="allowListEnabled"></domains-table>
</section> </section>
</main> </main>
</template> </template>
@ -32,12 +36,14 @@ import axios from 'axios'
import DomainsTable from "@/components/manage/moderation/DomainsTable" import DomainsTable from "@/components/manage/moderation/DomainsTable"
export default { export default {
props: ['allowListEnabled'],
components: { components: {
DomainsTable DomainsTable
}, },
data () { data () {
return { return {
domainName: '', domainName: '',
domainAllowed: this.allowListEnabled ? true : null,
isCreating: false, isCreating: false,
errors: [] errors: []
} }
@ -54,7 +60,7 @@ export default {
let self = this let self = this
this.isCreating = true this.isCreating = true
this.errors = [] this.errors = []
axios.post('manage/federation/domains/', {name: this.domainName}).then((response) => { axios.post('manage/federation/domains/', {name: this.domainName, allowed: this.domainAllowed}).then((response) => {
this.isCreating = false this.isCreating = false
this.$router.push({ this.$router.push({
name: "manage.moderation.domains.detail", name: "manage.moderation.domains.detail",