Commit 96360a39 authored by Eliot Berriot's avatar Eliot Berriot

Merge branch 'qa-017-round3' into 'develop'

Qa 017 round3

See merge request funkwhale/funkwhale!427
parents 42933fa1 02006270
......@@ -65,10 +65,6 @@ v1_patterns += [
r"^users/",
include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
),
url(
r"^requests/",
include(("funkwhale_api.requests.api_urls", "requests"), namespace="requests"),
),
url(r"^token/$", jwt_views.obtain_jwt_token, name="token"),
url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"),
]
......
......@@ -71,7 +71,15 @@ def update_uploads(libraries_by_user, stdout):
def update_orphan_uploads(open_api, stdout):
privacy_level = "everyone" if open_api else "instance"
first_superuser = User.objects.filter(is_superuser=True).order_by("pk").first()
first_superuser = (
User.objects.filter(is_superuser=True)
.exclude(actor=None)
.order_by("pk")
.first()
)
if not first_superuser:
stdout.write("* No superuser found, skipping update orphan uploads")
return
library, _ = models.Library.objects.get_or_create(
name="default",
actor=first_superuser.actor,
......
......@@ -2,7 +2,6 @@ from django_filters import rest_framework as filters
from funkwhale_api.common import fields
from funkwhale_api.music import models as music_models
from funkwhale_api.requests import models as requests_models
from funkwhale_api.users import models as users_models
......@@ -51,13 +50,3 @@ class ManageInvitationFilterSet(filters.FilterSet):
if value is None:
return queryset
return queryset.open(value)
class ManageImportRequestFilterSet(filters.FilterSet):
q = fields.SearchFilter(
search_fields=["user__username", "albums", "artist_name", "comment"]
)
class Meta:
model = requests_models.ImportRequest
fields = ["q", "status"]
from django.db import transaction
from django.utils import timezone
from rest_framework import serializers
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.music import models as music_models
from funkwhale_api.requests import models as requests_models
from funkwhale_api.users import models as users_models
from . import filters
......@@ -169,69 +168,3 @@ class ManageInvitationActionSerializer(common_serializers.ActionSerializer):
@transaction.atomic
def handle_delete(self, objects):
return objects.delete()
class ManageImportRequestSerializer(serializers.ModelSerializer):
user = ManageUserSimpleSerializer(required=False)
class Meta:
model = requests_models.ImportRequest
fields = [
"id",
"status",
"creation_date",
"imported_date",
"user",
"albums",
"artist_name",
"comment",
]
read_only_fields = [
"id",
"status",
"creation_date",
"imported_date",
"user",
"albums",
"artist_name",
"comment",
]
def validate_code(self, value):
if not value:
return value
if users_models.Invitation.objects.filter(code__iexact=value).exists():
raise serializers.ValidationError(
"An invitation with this code already exists"
)
return value
class ManageImportRequestActionSerializer(common_serializers.ActionSerializer):
actions = [
common_serializers.Action(
"mark_closed",
allow_all=True,
qs_filter=lambda qs: qs.filter(status__in=["pending", "accepted"]),
),
common_serializers.Action(
"mark_imported",
allow_all=True,
qs_filter=lambda qs: qs.filter(status__in=["pending", "accepted"]),
),
common_serializers.Action("delete", allow_all=False),
]
filterset_class = filters.ManageImportRequestFilterSet
@transaction.atomic
def handle_delete(self, objects):
return objects.delete()
@transaction.atomic
def handle_mark_closed(self, objects):
return objects.update(status="closed")
@transaction.atomic
def handle_mark_imported(self, objects):
now = timezone.now()
return objects.update(status="imported", imported_date=now)
......@@ -5,10 +5,6 @@ from . import views
library_router = routers.SimpleRouter()
library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
requests_router = routers.SimpleRouter()
requests_router.register(
r"import-requests", views.ManageImportRequestViewSet, "import-requests"
)
users_router = routers.SimpleRouter()
users_router.register(r"users", views.ManageUserViewSet, "users")
users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations")
......@@ -16,7 +12,4 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation
urlpatterns = [
url(r"^library/", include((library_router.urls, "instance"), namespace="library")),
url(r"^users/", include((users_router.urls, "instance"), namespace="users")),
url(
r"^requests/", include((requests_router.urls, "instance"), namespace="requests")
),
]
......@@ -3,7 +3,6 @@ from rest_framework.decorators import list_route
from funkwhale_api.common import preferences
from funkwhale_api.music import models as music_models
from funkwhale_api.requests import models as requests_models
from funkwhale_api.users import models as users_models
from funkwhale_api.users.permissions import HasUserPermission
......@@ -93,31 +92,3 @@ class ManageInvitationViewSet(
serializer.is_valid(raise_exception=True)
result = serializer.save()
return response.Response(result, status=200)
class ManageImportRequestViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet,
):
queryset = (
requests_models.ImportRequest.objects.all()
.order_by("-id")
.select_related("user")
)
serializer_class = serializers.ManageImportRequestSerializer
filter_class = filters.ManageImportRequestFilterSet
permission_classes = (HasUserPermission,)
required_permissions = ["library"]
ordering_fields = ["creation_date", "imported_date"]
@list_route(methods=["post"])
def action(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = serializers.ManageImportRequestActionSerializer(
request.data, queryset=queryset
)
serializer.is_valid(raise_exception=True)
result = serializer.save()
return response.Response(result, status=200)
from django.contrib import admin
from . import models
@admin.register(models.ImportRequest)
class ImportRequestAdmin(admin.ModelAdmin):
list_display = ["artist_name", "user", "status", "creation_date"]
list_select_related = ["user"]
list_filter = ["status"]
search_fields = ["artist_name", "comment", "albums"]
from rest_framework import routers
from . import views
router = routers.SimpleRouter()
router.register(r"import-requests", views.ImportRequestViewSet, "import-requests")
urlpatterns = router.urls
import factory
from funkwhale_api.factories import registry
from funkwhale_api.users.factories import UserFactory
@registry.register
class ImportRequestFactory(factory.django.DjangoModelFactory):
artist_name = factory.Faker("name")
albums = factory.Faker("sentence")
user = factory.SubFactory(UserFactory)
comment = factory.Faker("paragraph")
class Meta:
model = "requests.ImportRequest"
import django_filters
from funkwhale_api.common import fields
from . import models
class ImportRequestFilter(django_filters.FilterSet):
q = fields.SearchFilter(
search_fields=["artist_name", "user__username", "albums", "comment"]
)
class Meta:
model = models.ImportRequest
fields = {
"artist_name": ["exact", "iexact", "startswith", "icontains"],
"status": ["exact"],
"user__username": ["exact"],
}
from rest_framework import serializers
from funkwhale_api.users.serializers import UserBasicSerializer
from . import models
class ImportRequestSerializer(serializers.ModelSerializer):
user = UserBasicSerializer(read_only=True)
class Meta:
model = models.ImportRequest
fields = (
"id",
"status",
"albums",
"artist_name",
"user",
"creation_date",
"imported_date",
"comment",
)
read_only_fields = ("creation_date", "imported_date", "user", "status")
def create(self, validated_data):
validated_data["user"] = self.context["user"]
return super().create(validated_data)
from rest_framework import mixins, viewsets
from . import filters, models, serializers
class ImportRequestViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet,
):
serializer_class = serializers.ImportRequestSerializer
queryset = (
models.ImportRequest.objects.all().select_related().order_by("-creation_date")
)
filter_class = filters.ImportRequestFilter
ordering_fields = ("id", "artist_name", "creation_date", "status")
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
def get_serializer_context(self):
context = super().get_serializer_context()
if self.request.user.is_authenticated:
context["user"] = self.request.user
return context
......@@ -285,7 +285,7 @@ def create_actor(user):
args["private_key"] = private.decode("utf-8")
args["public_key"] = public.decode("utf-8")
return federation_models.Actor.objects.create(**args)
return federation_models.Actor.objects.create(user=user, **args)
@receiver(ldap_populate_user)
......
#!/bin/bash
#!/bin/bash -ex
OS_REQUIREMENTS_FILENAME="requirements.apt"
script_path=$(dirname "$(realpath $0)")
OS_REQUIREMENTS_FILENAME="$script_path/requirements.apt"
# Handle call with wrong command
function wrong_command()
......
......@@ -36,44 +36,3 @@ def test_user_update_permission(factories):
assert user.permission_upload is True
assert user.permission_library is False
assert user.permission_settings is True
def test_manage_import_request_mark_closed(factories):
affected = factories["requests.ImportRequest"].create_batch(
size=5, status="pending"
)
# we do not update imported requests
factories["requests.ImportRequest"].create_batch(size=5, status="imported")
s = serializers.ManageImportRequestActionSerializer(
queryset=affected[0].__class__.objects.all(),
data={"objects": "all", "action": "mark_closed"},
)
assert s.is_valid(raise_exception=True) is True
s.save()
assert affected[0].__class__.objects.filter(status="imported").count() == 5
for ir in affected:
ir.refresh_from_db()
assert ir.status == "closed"
def test_manage_import_request_mark_imported(factories, now):
affected = factories["requests.ImportRequest"].create_batch(
size=5, status="pending"
)
# we do not update closed requests
factories["requests.ImportRequest"].create_batch(size=5, status="closed")
s = serializers.ManageImportRequestActionSerializer(
queryset=affected[0].__class__.objects.all(),
data={"objects": "all", "action": "mark_imported"},
)
assert s.is_valid(raise_exception=True) is True
s.save()
assert affected[0].__class__.objects.filter(status="closed").count() == 5
for ir in affected:
ir.refresh_from_db()
assert ir.status == "imported"
assert ir.imported_date == now
......@@ -10,7 +10,6 @@ from funkwhale_api.manage import serializers, views
(views.ManageUploadViewSet, ["library"], "and"),
(views.ManageUserViewSet, ["settings"], "and"),
(views.ManageInvitationViewSet, ["settings"], "and"),
(views.ManageImportRequestViewSet, ["library"], "and"),
],
)
def test_permissions(assert_user_permission, view, permissions, operator):
......@@ -65,15 +64,3 @@ def test_invitation_view_create(factories, superuser_api_client, mocker):
assert response.status_code == 201
assert superuser_api_client.user.invitations.latest("id") is not None
def test_music_requests_view(factories, superuser_api_client, mocker):
invitations = factories["requests.ImportRequest"].create_batch(size=5)
qs = invitations[0].__class__.objects.order_by("-id")
url = reverse("api:v1:manage:requests:import-requests-list")
response = superuser_api_client.get(url, {"sort": "-id"})
expected = serializers.ManageImportRequestSerializer(qs, many=True).data
assert response.data["count"] == len(invitations)
assert response.data["results"] == expected
from django.urls import reverse
def test_request_viewset_requires_auth(db, api_client):
url = reverse("api:v1:requests:import-requests-list")
response = api_client.get(url)
assert response.status_code == 401
def test_user_can_create_request(logged_in_api_client):
url = reverse("api:v1:requests:import-requests-list")
user = logged_in_api_client.user
data = {
"artist_name": "System of a Down",
"albums": "All please!",
"comment": "Please, they rock!",
}
response = logged_in_api_client.post(url, data)
assert response.status_code == 201
ir = user.import_requests.latest("id")
assert ir.status == "pending"
assert ir.creation_date is not None
for field, value in data.items():
assert getattr(ir, field) == value
......@@ -189,10 +189,14 @@ then run the migrations script.
On docker-setups::
# if you missed this one from a previous upgrade
docker-compose run --rm api python manage.py script create_actors --no-input
docker-compose run --rm api python manage.py script migrate_to_user_libraries --no-input
On non docker-setups::
# if you missed this one from a previous upgrade
python api/manage.py script create_actors --no-input
python api/manage.py script migrate_to_user_libraries --no-input
If the scripts ends without errors, you're instance should be updated and ready to use :)
......
......@@ -34,9 +34,6 @@
<router-link class="item" to="/about">
<translate>About this instance</translate>
</router-link>
<router-link class="item" :to="{name: 'library.request'}">
<translate>Request music</translate>
</router-link>
<a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a>
<a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
......
......@@ -27,12 +27,10 @@ import Favorites from '@/components/favorites/List'
import AdminSettings from '@/views/admin/Settings'
import AdminLibraryBase from '@/views/admin/library/Base'
import AdminLibraryFilesList from '@/views/admin/library/FilesList'
import AdminLibraryRequestsList from '@/views/admin/library/RequestsList'
import AdminUsersBase from '@/views/admin/users/Base'
import AdminUsersDetail from '@/views/admin/users/UsersDetail'
import AdminUsersList from '@/views/admin/users/UsersList'
import AdminInvitationsList from '@/views/admin/users/InvitationsList'
import MusicRequest from '@/views/library/MusicRequest'
import FederationBase from '@/views/federation/Base'
import FederationScan from '@/views/federation/Scan'
import FederationLibraryDetail from '@/views/federation/LibraryDetail'
......@@ -257,11 +255,6 @@ export default new Router({
path: 'files',
name: 'manage.library.files',
component: AdminLibraryFilesList
},
{
path: 'requests',
name: 'manage.library.requests',
component: AdminLibraryRequestsList
}
]
},
......@@ -292,11 +285,6 @@ export default new Router({
component: Library,
children: [
{ path: '', component: LibraryHome, name: 'library.index' },
{
path: 'requests/',
name: 'library.request',
component: MusicRequest
},
{
path: 'artists/',
name: 'library.artists.browse',
......
......@@ -79,7 +79,6 @@ export default {
// somehow, extraction fails if in the return block directly
let instanceLabel = this.$gettext('Instance information')
let usersLabel = this.$gettext('Users')
let importsLabel = this.$gettext('Imports')
let playlistsLabel = this.$gettext('Playlists')
let federationLabel = this.$gettext('Federation')
let subsonicLabel = this.$gettext('Subsonic')
......@@ -105,13 +104,6 @@ export default {
'users__upload_quota'
]
},
{
label: importsLabel,
id: 'imports',
settings: [
'providers_youtube__api_key'
]
},
{
label: playlistsLabel,
id: 'playlists',
......
......@@ -4,15 +4,6 @@
<router-link
class="ui item"
:to="{name: 'manage.library.files'}"><translate>Files</translate></router-link>
<router-link
class="ui item"
:to="{name: 'manage.library.requests'}">
<translate>Import requests</translate>
<div
:class="['ui', {'teal': $store.state.ui.notifications.importRequests > 0}, 'label']"
:title="labels.pendingRequests">
{{ $store.state.ui.notifications.importRequests }}</div>
</router-link>
</div>
<router-view :key="$route.fullPath"></router-view>
</div>
......@@ -23,10 +14,8 @@ export default {
computed: {
labels () {
let title = this.$gettext('Manage library')
let pendingRequests = this.$gettext('Pending import requests')
return {
title,
pendingRequests
title
}
}
}
......
<template>
<div v-title="labels.importRequests">
<div class="ui vertical stripe segment">
<h2 class="ui header"><translate>Import requests</translate></h2>
<div class="ui hidden divider"></div>
<library-requests-table></library-requests-table>
</div>
</div>
</template>
<script>
import LibraryRequestsTable from '@/components/manage/library/RequestsTable'
export default {
components: {
LibraryRequestsTable
},
computed: {
labels () {
return {
importRequests: this.$gettext('Import requests')
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
......@@ -36,8 +36,8 @@
<i class="database icon"></i>
{{ library.size | humanSize }}
</span>
<i class="music icon"></i>
<translate :translate-params="{count: library.uploads_count}" :translate-n="library.uploads_count" translate-plural="%{ count } tracks">1 tracks</translate>
<i class="music icon"></i> {{ library.uploads_count }}
<translate :translate-params="{count: library.uploads_count}" :translate-n="library.uploads_count" translate-plural="%{ count } tracks">1 track</translate>
</div>
</div>
<div class="ui bottom basic attached buttons">
......
<template>
<div class="ui vertical stripe segment" v-title="labels.title">
<div class="ui small text container">
<h2 class="ui header">
<translate>Request some music</translate>
</h2>
<request-form v-if="$store.state.auth.authenticated"></request-form>
</div>
</div>
</template>
<script>
import RequestForm from '@/components/requests/Form'
export default {
components: {
RequestForm
},
computed: {
labels () {
let title = this.$gettext('Request some music')
return {
title
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment