Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
interfect
funkwhale
Commits
fc09a3b3
Verified
Commit
fc09a3b3
authored
Apr 14, 2018
by
Eliot Berriot
Browse files
Can now accept/deny follow requests
parent
ca02aca3
Changes
11
Show whitespace changes
Inline
Side-by-side
api/funkwhale_api/federation/filters.py
View file @
fc09a3b3
...
...
@@ -43,6 +43,7 @@ class LibraryTrackFilter(django_filters.FilterSet):
class
FollowFilter
(
django_filters
.
FilterSet
):
pending
=
django_filters
.
CharFilter
(
method
=
'filter_pending'
)
ordering
=
django_filters
.
OrderingFilter
(
# tuple-mapping retains order
fields
=
(
...
...
@@ -50,9 +51,16 @@ class FollowFilter(django_filters.FilterSet):
(
'modification_date'
,
'modification_date'
),
),
)
q
=
fields
.
SearchFilter
(
search_fields
=
[
'actor__domain'
,
'actor__preferred_username'
,
])
class
Meta
:
model
=
models
.
Follow
fields
=
{
'approved'
:
[
'exact'
],
}
fields
=
[
'approved'
,
'pending'
,
'q'
]
def
filter_pending
(
self
,
queryset
,
field_name
,
value
):
if
value
.
lower
()
in
[
'true'
,
'1'
,
'yes'
]:
queryset
=
queryset
.
filter
(
approved__isnull
=
True
)
return
queryset
api/funkwhale_api/federation/serializers.py
View file @
fc09a3b3
...
...
@@ -190,6 +190,35 @@ class APILibraryScanSerializer(serializers.Serializer):
until
=
serializers
.
DateTimeField
(
required
=
False
)
class
APILibraryFollowUpdateSerializer
(
serializers
.
Serializer
):
follow
=
serializers
.
IntegerField
()
approved
=
serializers
.
BooleanField
()
def
validate_follow
(
self
,
value
):
from
.
import
actors
library_actor
=
actors
.
SYSTEM_ACTORS
[
'library'
].
get_actor_instance
()
qs
=
models
.
Follow
.
objects
.
filter
(
pk
=
value
,
target
=
library_actor
,
)
try
:
return
qs
.
get
()
except
models
.
Follow
.
DoesNotExist
:
raise
serializers
.
ValidationError
(
'Invalid follow'
)
def
save
(
self
):
new_status
=
self
.
validated_data
[
'approved'
]
follow
=
self
.
validated_data
[
'follow'
]
if
new_status
==
follow
.
approved
:
return
follow
follow
.
approved
=
new_status
follow
.
save
(
update_fields
=
[
'approved'
,
'modification_date'
])
if
new_status
:
activity
.
accept_follow
(
follow
)
return
follow
class
APILibraryCreateSerializer
(
serializers
.
ModelSerializer
):
actor
=
serializers
.
URLField
()
federation_enabled
=
serializers
.
BooleanField
()
...
...
@@ -233,8 +262,13 @@ class APILibraryCreateSerializer(serializers.ModelSerializer):
library_data
=
library
.
get_library_data
(
acs
.
validated_data
[
'library_url'
])
if
'errors'
in
library_data
:
raise
serializers
.
ValidationError
(
str
(
library_data
[
'errors'
]))
# we pass silently because it may means we require permission
# before scanning
pass
validated_data
[
'library'
]
=
library_data
validated_data
[
'library'
].
setdefault
(
'id'
,
acs
.
validated_data
[
'library_url'
]
)
validated_data
[
'actor'
]
=
actor
return
validated_data
...
...
@@ -244,7 +278,7 @@ class APILibraryCreateSerializer(serializers.ModelSerializer):
defaults
=
{
'actor'
:
validated_data
[
'actor'
],
'follow'
:
validated_data
[
'follow'
],
'tracks_count'
:
validated_data
[
'library'
]
[
'totalItems'
]
,
'tracks_count'
:
validated_data
[
'library'
]
.
get
(
'totalItems'
)
,
'federation_enabled'
:
validated_data
[
'federation_enabled'
],
'autoimport'
:
validated_data
[
'autoimport'
],
'download_files'
:
validated_data
[
'download_files'
],
...
...
api/funkwhale_api/federation/views.py
View file @
fc09a3b3
...
...
@@ -221,31 +221,42 @@ class LibraryViewSet(
queryset
=
models
.
Follow
.
objects
.
filter
(
actor
=
library_actor
).
select_related
(
'
target
'
,
'
actor
'
,
'target'
,
).
order_by
(
'-creation_date'
)
filterset
=
filters
.
FollowFilter
(
request
.
GET
,
queryset
=
queryset
)
serializer
=
serializers
.
APIFollowSerializer
(
filterset
.
qs
,
many
=
True
)
final_qs
=
filterset
.
qs
serializer
=
serializers
.
APIFollowSerializer
(
final_qs
,
many
=
True
)
data
=
{
'results'
:
serializer
.
data
,
'count'
:
len
(
fi
lterset
.
qs
),
'count'
:
len
(
fi
nal_
qs
),
}
return
response
.
Response
(
data
)
@
list_route
(
methods
=
[
'get'
])
@
list_route
(
methods
=
[
'get'
,
'patch'
])
def
followers
(
self
,
request
,
*
args
,
**
kwargs
):
if
request
.
method
.
lower
()
==
'patch'
:
serializer
=
serializers
.
APILibraryFollowUpdateSerializer
(
data
=
request
.
data
)
serializer
.
is_valid
(
raise_exception
=
True
)
follow
=
serializer
.
save
()
return
response
.
Response
(
serializers
.
APIFollowSerializer
(
follow
).
data
)
library_actor
=
actors
.
SYSTEM_ACTORS
[
'library'
].
get_actor_instance
()
queryset
=
models
.
Follow
.
objects
.
filter
(
target
=
library_actor
).
select_related
(
'
target
'
,
'
actor
'
,
'target'
,
).
order_by
(
'-creation_date'
)
filterset
=
filters
.
FollowFilter
(
request
.
GET
,
queryset
=
queryset
)
serializer
=
serializers
.
APIFollowSerializer
(
filterset
.
qs
,
many
=
True
)
final_qs
=
filterset
.
qs
serializer
=
serializers
.
APIFollowSerializer
(
final_qs
,
many
=
True
)
data
=
{
'results'
:
serializer
.
data
,
'count'
:
len
(
fi
lterset
.
qs
),
'count'
:
len
(
fi
nal_
qs
),
}
return
response
.
Response
(
data
)
...
...
api/tests/federation/test_views.py
View file @
fc09a3b3
...
...
@@ -346,3 +346,37 @@ def test_list_library_tracks(factories, superuser_api_client):
'previous'
:
None
,
'next'
:
None
,
}
def
test_can_update_follow_status
(
factories
,
superuser_api_client
,
mocker
):
patched_accept
=
mocker
.
patch
(
'funkwhale_api.federation.activity.accept_follow'
)
library_actor
=
actors
.
SYSTEM_ACTORS
[
'library'
].
get_actor_instance
()
follow
=
factories
[
'federation.Follow'
](
target
=
library_actor
)
payload
=
{
'follow'
:
follow
.
pk
,
'approved'
:
True
}
url
=
reverse
(
'api:v1:federation:libraries-followers'
)
response
=
superuser_api_client
.
patch
(
url
,
payload
)
follow
.
refresh_from_db
()
assert
response
.
status_code
==
200
assert
follow
.
approved
is
True
patched_accept
.
assert_called_once_with
(
follow
)
def
test_can_filter_pending_follows
(
factories
,
superuser_api_client
):
library_actor
=
actors
.
SYSTEM_ACTORS
[
'library'
].
get_actor_instance
()
follow
=
factories
[
'federation.Follow'
](
target
=
library_actor
,
approved
=
True
)
params
=
{
'pending'
:
True
}
url
=
reverse
(
'api:v1:federation:libraries-followers'
)
response
=
superuser_api_client
.
get
(
url
,
params
)
assert
response
.
status_code
==
200
assert
len
(
response
.
data
[
'results'
])
==
0
front/src/components/common/DangerousButton.vue
View file @
fc09a3b3
...
...
@@ -26,7 +26,7 @@ import Modal from '@/components/semantic/Modal'
export
default
{
props
:
{
action
:
{
type
:
Function
,
required
:
tru
e
},
action
:
{
type
:
Function
,
required
:
fals
e
},
disabled
:
{
type
:
Boolean
,
default
:
false
},
color
:
{
type
:
String
,
default
:
'
red
'
}
},
...
...
@@ -41,8 +41,11 @@ export default {
methods
:
{
confirm
()
{
this
.
showModal
=
false
this
.
$emit
(
'
confirm
'
)
if
(
this
.
action
)
{
this
.
action
()
}
}
}
}
</
script
>
front/src/components/federation/LibraryCard.vue
View file @
fc09a3b3
...
...
@@ -15,7 +15,7 @@
<span
class=
"right floated"
v-else
>
<i
class=
"open lock icon"
></i>
Open
</span>
<span>
<span
v-if=
"totalItems"
>
<i
class=
"music icon"
></i>
{{
totalItems
}}
tracks
</span>
...
...
@@ -25,10 +25,6 @@
<i
class=
"clock icon"
></i>
Follow request pending approval
</
template
>
<
template
v-else-if=
"following"
>
<i
class=
"check icon"
></i>
Already following this library
</
template
>
<div
v-if=
"!library"
@
click=
"follow"
...
...
front/src/components/federation/LibraryFollowTable.vue
0 → 100644
View file @
fc09a3b3
<
template
>
<div>
<div
class=
"ui form"
>
<div
class=
"fields"
>
<div
class=
"ui six wide field"
>
<input
type=
"text"
v-model=
"search"
placeholder=
"Search by username, domain..."
/>
</div>
<div
class=
"ui four wide inline field"
>
<div
class=
"ui checkbox"
>
<input
v-model=
"pending"
type=
"checkbox"
>
<label>
Pending approval
</label>
</div>
</div>
</div>
</div>
<div
class=
"ui hidden divider"
></div>
<table
v-if=
"result"
class=
"ui very basic single line unstackable table"
>
<thead>
<tr>
<th>
Actor
</th>
<th>
Creation date
</th>
<th>
Status
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
<tr
v-for=
"follow in result.results"
>
<td>
{{
follow
.
actor
.
preferred_username
}}
@
{{
follow
.
actor
.
domain
}}
</td>
<td>
<human-date
:date=
"follow.creation_date"
></human-date>
</td>
<td>
<template
v-if=
"follow.approved === true"
>
<i
class=
"check icon"
></i>
Approved
</
template
>
<
template
v-else-if=
"follow.approved === false"
>
<i
class=
"x icon"
></i>
Refused
</
template
>
<
template
v-else
>
<i
class=
"clock icon"
></i>
Pending
</
template
>
</td>
<td>
<dangerous-button
v-if=
"follow.approved !== false"
class=
"tiny basic labeled icon"
color=
'red'
@
confirm=
"updateFollow(follow, false)"
>
<i
class=
"x icon"
></i>
Deny
<p
slot=
"modal-header"
>
Deny access?
</p>
<p
slot=
"modal-content"
>
By confirming, {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} will be denied access to your library.
</p>
<p
slot=
"modal-confirm"
>
Deny
</p>
</dangerous-button>
<dangerous-button
v-if=
"follow.approved !== true"
class=
"tiny basic labeled icon"
color=
'green'
@
confirm=
"updateFollow(follow, true)"
>
<i
class=
"x icon"
></i>
Approve
<p
slot=
"modal-header"
>
Approve access?
</p>
<p
slot=
"modal-content"
>
By confirming, {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} will be granted access to your library.
</p>
<p
slot=
"modal-confirm"
>
Approve
</p>
</dangerous-button>
</td>
</tr>
</tbody>
<tfoot
class=
"full-width"
>
<tr>
<th>
<pagination
v-if=
"result && result.results.length > 0"
@
page-changed=
"selectPage"
:compact=
"true"
:current=
"page"
:paginate-by=
"paginateBy"
:total=
"result.count"
></pagination>
</th>
<th
v-if=
"result && result.results.length > 0"
>
Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }}
</th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</template>
<
script
>
import
axios
from
'
axios
'
import
_
from
'
lodash
'
import
Pagination
from
'
@/components/Pagination
'
export
default
{
props
:
{
filters
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
{}}
},
components
:
{
Pagination
},
data
()
{
return
{
isLoading
:
false
,
result
:
null
,
page
:
1
,
paginateBy
:
25
,
search
:
''
,
pending
:
false
}
},
created
()
{
this
.
fetchData
()
},
methods
:
{
fetchData
()
{
let
params
=
_
.
merge
({
'
page
'
:
this
.
page
,
'
page_size
'
:
this
.
paginateBy
,
'
q
'
:
this
.
search
},
this
.
filters
)
if
(
this
.
pending
)
{
params
.
pending
=
true
}
let
self
=
this
self
.
isLoading
=
true
axios
.
get
(
'
/federation/libraries/followers/
'
,
{
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
},
updateFollow
(
follow
,
approved
)
{
let
payload
=
{
follow
:
follow
.
id
,
approved
:
approved
}
let
self
=
this
axios
.
patch
(
'
/federation/libraries/followers/
'
,
payload
).
then
((
response
)
=>
{
follow
.
approved
=
response
.
data
.
approved
self
.
isLoading
=
false
},
error
=>
{
self
.
isLoading
=
false
self
.
errors
=
error
.
backendErrors
})
}
},
watch
:
{
search
(
newValue
)
{
if
(
newValue
.
length
>
0
)
{
this
.
fetchData
()
}
},
page
()
{
this
.
fetchData
()
},
pending
()
{
this
.
fetchData
()
}
}
}
</
script
>
front/src/components/federation/LibraryTrackTable.vue
View file @
fc09a3b3
...
...
@@ -64,13 +64,19 @@
></pagination>
</th>
<th>
Showing results
{{
((
page
-
1
)
*
paginateBy
)
+
1
}}
-
{{
((
page
-
1
)
*
paginateBy
)
+
result
.
results
.
length
}}
on
{{
result
.
count
}}
</th>
<th
v-if=
"result && result.results.length > 0"
>
Showing results
{{
((
page
-
1
)
*
paginateBy
)
+
1
}}
-
{{
((
page
-
1
)
*
paginateBy
)
+
result
.
results
.
length
}}
on
{{
result
.
count
}}
</th>
<th>
<button
@
click=
"launchImport"
:disabled=
"checked.length === 0 || isImporting"
:class=
"['ui', 'green',
{loading: isImporting}, 'button']">Import
{{
checked
.
length
}}
tracks
</button>
<router-link
v-if=
"importBatch"
:to=
"
{name: 'library.import.batches.detail', params: {id: importBatch.id }}">
Import #
{{
importBatch
.
id
}}
launched
</router-link>
</th>
<th></th>
<th></th>
...
...
@@ -104,7 +110,8 @@ export default {
paginateBy
:
25
,
search
:
''
,
checked
:
{},
isImporting
:
false
isImporting
:
false
,
importBatch
:
null
}
},
created
()
{
...
...
@@ -135,6 +142,7 @@ export default {
library_tracks
:
this
.
checked
}
axios
.
post
(
'
/submit/federation/
'
,
payload
).
then
((
response
)
=>
{
self
.
importBatch
=
response
.
data
self
.
isImporting
=
false
self
.
fetchData
()
},
error
=>
{
...
...
front/src/router/index.js
View file @
fc09a3b3
...
...
@@ -30,6 +30,7 @@ import FederationScan from '@/views/federation/Scan'
import
FederationLibraryDetail
from
'
@/views/federation/LibraryDetail
'
import
FederationLibraryList
from
'
@/views/federation/LibraryList
'
import
FederationTrackList
from
'
@/views/federation/LibraryTrackList
'
import
FederationFollowersList
from
'
@/views/federation/LibraryFollowersList
'
Vue
.
use
(
Router
)
...
...
@@ -118,6 +119,17 @@ export default new Router({
defaultPage
:
route
.
query
.
page
})
},
{
path
:
'
followers
'
,
name
:
'
federation.followers.list
'
,
component
:
FederationFollowersList
,
props
:
(
route
)
=>
({
defaultOrdering
:
route
.
query
.
ordering
,
defaultQuery
:
route
.
query
.
query
,
defaultPaginateBy
:
route
.
query
.
paginateBy
,
defaultPage
:
route
.
query
.
page
})
},
{
path
:
'
libraries/:id
'
,
name
:
'
federation.libraries.detail
'
,
component
:
FederationLibraryDetail
,
props
:
true
}
]
},
...
...
front/src/views/federation/Base.vue
View file @
fc09a3b3
...
...
@@ -7,10 +7,39 @@
<router-link
class=
"ui item"
:to=
"
{name: 'federation.tracks.list'}">Tracks
</router-link>
<div
class=
"ui secondary right menu"
>
<router-link
class=
"ui item"
:to=
"
{name: 'federation.followers.list'}">
Followers
<div
class=
"ui teal label"
title=
"Pending requests"
>
{{
requestsCount
}}
</div>
</router-link>
</div>
</div>
<router-view
:key=
"$route.fullPath"
></router-view>
</div>
</
template
>
<
script
>
import
axios
from
'
axios
'
export
default
{
data
()
{
return
{
requestsCount
:
0
}
},
created
()
{
this
.
fetchRequestsCount
()
},
methods
:
{
fetchRequestsCount
()
{
let
self
=
this
axios
.
get
(
'
federation/libraries/followers/
'
,
{
params
:
{
pending
:
true
}}).
then
(
response
=>
{
self
.
requestsCount
=
response
.
data
.
count
})
}
}
}
</
script
>
<
style
lang=
"scss"
>
@import
'../../style/vendor/media'
;
...
...
front/src/views/federation/LibraryFollowersList.vue
0 → 100644
View file @
fc09a3b3
<
template
>
<div
v-title=
"'Followers'"
>
<div
class=
"ui vertical stripe segment"
>
<h2
class=
"ui header"
>
Browsing followers
</h2>
<p>
Be careful when accepting follow requests, as it means the follower
will have access to your entire library.
</p>
<div
class=
"ui hidden divider"
></div>
<library-follow-table></library-follow-table>
</div>
</div>
</
template
>
<
script
>
import
LibraryFollowTable
from
'
@/components/federation/LibraryFollowTable
'
export
default
{
components
:
{
LibraryFollowTable
}
}
</
script
>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<
style
scoped
>
</
style
>
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment